From f735fb0551812fd781a2db8bac5a0deef4cabb2b Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:54:14 +0530 Subject: [PATCH 001/443] feat(router): add fallback while add card and retrieve card from rust locker (#2888) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .typos.toml | 1 + config/config.example.toml | 199 +++++++++--------- crates/router/src/core/locker_migration.rs | 2 +- .../router/src/core/payment_methods/cards.rs | 119 ++++++++--- .../src/core/payment_methods/transformers.rs | 12 +- .../router/src/core/payments/tokenization.rs | 4 +- crates/router/src/core/payouts/helpers.rs | 2 +- 7 files changed, 208 insertions(+), 131 deletions(-) diff --git a/.typos.toml b/.typos.toml index 1ac38a005c9e..4ce21526604b 100644 --- a/.typos.toml +++ b/.typos.toml @@ -24,6 +24,7 @@ optin = "optin" # Boku preflow name optin_id = "optin_id" # Boku's id for optin flow deriver = "deriver" Deriver = "Deriver" +requestor_card_reference = "requestor_card_reference" [default.extend-words] aci = "aci" # Name of a connector diff --git a/config/config.example.toml b/config/config.example.toml index 02eff1d42979..7815f2400d04 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -21,25 +21,25 @@ idle_pool_connection_timeout = 90 # Timeout for idle pool connections (defaults # Main SQL data store credentials [master_database] -username = "db_user" # DB Username -password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds -queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client +username = "db_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Replica SQL data store credentials [replica_database] -username = "replica_user" # DB Username -password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled -host = "localhost" # DB Host -port = 5432 # DB Port -dbname = "hyperswitch_db" # Name of Database -pool_size = 5 # Number of connections to keep open -connection_timeout = 10 # Timeout for database connection in seconds -queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client +username = "replica_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Redis credentials [redis] @@ -95,17 +95,17 @@ sampling_rate = 0.1 # decimal rate between 0.0 otel_exporter_otlp_endpoint = "http://localhost:4317" # endpoint to send metrics and traces to, can include port number otel_exporter_otlp_timeout = 5000 # timeout (in milliseconds) for sending metrics and traces use_xray_generator = false # Set this to true for AWS X-ray compatible traces -route_to_trace = [ "*/confirm" ] +route_to_trace = ["*/confirm"] # This section provides some secret values. [secrets] -master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. -admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. -kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. -jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. -kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. -recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. -kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled +master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. +admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. +kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. +jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. +kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. +recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. +kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled # Locker settings contain details for accessing a card locker, a # PCI Compliant storage entity which stores payment method information @@ -124,15 +124,15 @@ connectors_with_delayed_session_response = "trustpay,payme" # List of connectors connectors_with_webhook_source_verification_call = "paypal" # List of connectors which has additional source verification api-call [jwekey] # 4 priv/pub key pair -locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk -locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk -locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk -locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk -locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk -locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk -vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs +locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk +locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk +locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk +locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk +locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk +locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk +vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker -vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs +vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs # Refund configuration @@ -234,11 +234,11 @@ adyen = { banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" } # Bank redirect configs for allowed banks through online_banking_poland payment method [bank_config.online_banking_poland] -adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24"} +adyen = { banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" } # Bank redirect configs for allowed banks through open_banking_uk payment method [bank_config.open_banking_uk] -adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled"} +adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" } # Bank redirect configs for allowed banks through przelewy24 payment method [bank_config.przelewy24] @@ -313,89 +313,92 @@ region = "" # The AWS region used by the KMS SDK for decrypting data. # EmailClient configuration. Only applicable when the `email` feature flag is enabled. [email] from_email = "notify@example.com" # Sender email -aws_region = "" # AWS region used by AWS SES -base_url = "" # Base url used when adding links that should redirect to self +aws_region = "" # AWS region used by AWS SES +base_url = "" # Base url used when adding links that should redirect to self #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet" } -mollie = {long_lived_token = false, payment_method = "card"} +mollie = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } -square = {long_lived_token = false, payment_method = "card"} +square = { long_lived_token = false, payment_method = "card" } braintree = { long_lived_token = false, payment_method = "card" } -gocardless = {long_lived_token = true, payment_method = "bank_debit"} +gocardless = { long_lived_token = true, payment_method = "bank_debit" } [temp_locker_enable_config] -stripe = {payment_method = "bank_transfer"} -nuvei = {payment_method = "card"} -shift4 = {payment_method = "card"} -bluesnap = {payment_method = "card"} +stripe = { payment_method = "bank_transfer" } +nuvei = { payment_method = "card" } +shift4 = { payment_method = "card" } +bluesnap = { payment_method = "card" } [dummy_connector] -enabled = true # Whether dummy connector is enabled or not -payment_ttl = 172800 # Time to live for dummy connector payment in redis -payment_duration = 1000 # Fake delay duration for dummy connector payment -payment_tolerance = 100 # Fake delay tolerance for dummy connector payment -payment_retrieve_duration = 500 # Fake delay duration for dummy connector payment sync -payment_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector payment sync -payment_complete_duration = 500 # Fake delay duration for dummy connector payment complete -payment_complete_tolerance = 100 # Fake delay tolerance for dummy connector payment complete -refund_ttl = 172800 # Time to live for dummy connector refund in redis -refund_duration = 1000 # Fake delay duration for dummy connector refund -refund_tolerance = 100 # Fake delay tolerance for dummy connector refund -refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync -refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync -authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis +enabled = true # Whether dummy connector is enabled or not +payment_ttl = 172800 # Time to live for dummy connector payment in redis +payment_duration = 1000 # Fake delay duration for dummy connector payment +payment_tolerance = 100 # Fake delay tolerance for dummy connector payment +payment_retrieve_duration = 500 # Fake delay duration for dummy connector payment sync +payment_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector payment sync +payment_complete_duration = 500 # Fake delay duration for dummy connector payment complete +payment_complete_tolerance = 100 # Fake delay tolerance for dummy connector payment complete +refund_ttl = 172800 # Time to live for dummy connector refund in redis +refund_duration = 1000 # Fake delay duration for dummy connector refund +refund_tolerance = 100 # Fake delay tolerance for dummy connector refund +refund_retrieve_duration = 500 # Fake delay duration for dummy connector refund sync +refund_retrieve_tolerance = 100 # Fake delay tolerance for dummy connector refund sync +authorize_ttl = 36000 # Time to live for dummy connector authorize request in redis assets_base_url = "https://www.example.com/" # Base url for dummy connector assets default_return_url = "https://www.example.com/" # Default return url when no return url is passed while payment slack_invite_url = "https://www.example.com/" # Slack invite url for hyperswitch discord_invite_url = "https://www.example.com/" # Discord invite url for hyperswitch [mandates.supported_payment_methods] -card.credit = {connector_list = "stripe,adyen"} # Mandate supported payment method type and connector for card -wallet.paypal = {connector_list = "adyen"} # Mandate supported payment method type and connector for wallets -pay_later.klarna = {connector_list = "adyen"} # Mandate supported payment method type and connector for pay_later -bank_debit.ach = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless"} # Mandate supported payment method type and connector for bank_debit +card.credit = { connector_list = "stripe,adyen" } # Mandate supported payment method type and connector for card +wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets +pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later +bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit # Required fields info used while listing the payment_method_data [required_fields.pay_later] # payment_method = "pay_later" -afterpay_clearpay = {fields = {stripe = [ # payment_method_type = afterpay_clearpay, connector = "stripe" - # Required fields vector with its respective display name in front-end and field_type - { required_field = "shipping.address.first_name", display_name = "first_name", field_type = "text" }, - { required_field = "shipping.address.last_name", display_name = "last_name", field_type = "text" }, - { required_field = "shipping.address.country", display_name = "country", field_type = { drop_down = { options = [ "US", "IN" ] } } }, - ] } } +afterpay_clearpay = { fields = { stripe = [ # payment_method_type = afterpay_clearpay, connector = "stripe" + # Required fields vector with its respective display name in front-end and field_type + { required_field = "shipping.address.first_name", display_name = "first_name", field_type = "text" }, + { required_field = "shipping.address.last_name", display_name = "last_name", field_type = "text" }, + { required_field = "shipping.address.country", display_name = "country", field_type = { drop_down = { options = [ + "US", + "IN", + ] } } }, +] } } [payouts] -payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility +payout_eligibility = true # Defaults the eligibility of a payout method to true in case connector does not provide checks for payout eligibility [pm_filters.adyen] -online_banking_fpx = {country = "MY", currency = "MYR"} -online_banking_thailand = {country = "TH", currency = "THB"} -touch_n_go = {country = "MY", currency = "MYR"} -atome = {country = "MY,SG", currency = "MYR,SGD"} -swish = {country = "SE", currency = "SEK"} -permata_bank_transfer = {country = "ID", currency = "IDR"} -bca_bank_transfer = {country = "ID", currency = "IDR"} -bni_va = {country = "ID", currency = "IDR"} -bri_va = {country = "ID", currency = "IDR"} -cimb_va = {country = "ID", currency = "IDR"} -danamon_va = {country = "ID", currency = "IDR"} -mandiri_va = {country = "ID", currency = "IDR"} -alfamart = {country = "ID", currency = "IDR"} -indomaret = {country = "ID", currency = "IDR"} -open_banking_uk = {country = "GB", currency = "GBP"} -oxxo = {country = "MX", currency = "MXN"} -pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} -seven_eleven = {country = "JP", currency = "JPY"} -lawson = {country = "JP", currency = "JPY"} -mini_stop = {country = "JP", currency = "JPY"} -family_mart = {country = "JP", currency = "JPY"} -seicomart = {country = "JP", currency = "JPY"} -pay_easy = {country = "JP", currency = "JPY"} +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_thailand = { country = "TH", currency = "THB" } +touch_n_go = { country = "MY", currency = "MYR" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +swish = { country = "SE", currency = "SEK" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bni_va = { country = "ID", currency = "IDR" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +mandiri_va = { country = "ID", currency = "IDR" } +alfamart = { country = "ID", currency = "IDR" } +indomaret = { country = "ID", currency = "IDR" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +seven_eleven = { country = "JP", currency = "JPY" } +lawson = { country = "JP", currency = "JPY" } +mini_stop = { country = "JP", currency = "JPY" } +family_mart = { country = "JP", currency = "JPY" } +seicomart = { country = "JP", currency = "JPY" } +pay_easy = { country = "JP", currency = "JPY" } [pm_filters.zen] credit = { not_available_flows = { capture_method = "manual" } } @@ -415,7 +418,7 @@ debit = { currency = "USD" } ach = { currency = "USD" } [pm_filters.stripe] -cashapp = {country = "US", currency = "USD"} +cashapp = { country = "US", currency = "USD" } [pm_filters.prophetpay] card_redirect = { currency = "USD" } @@ -434,10 +437,10 @@ adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_ba supported_connectors = "braintree" [applepay_decrypt_keys] -apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate -apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve -apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate -apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index aa82b4a3a636..f036a03a2f0e 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -106,7 +106,7 @@ pub async fn call_to_locker( let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs( state, pm_create, - card_details, + &card_details, customer_id.to_string(), merchant_account, api_enums::LockerChoice::Tartarus, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f9c666cbb954..4ab7d334f883 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -103,16 +103,12 @@ pub async fn add_payment_method( let merchant_id = &merchant_account.merchant_id; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; let response = match req.card.clone() { - Some(card) => add_card_to_locker( - &state, - req.clone(), - card, - customer_id.clone(), - merchant_account, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Add Card Failed"), + Some(card) => { + add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card Failed") + } None => { let pm_id = generate_id(consts::ID_LENGTH, "pm"); let payment_method_response = api::PaymentMethodResponse { @@ -207,18 +203,18 @@ pub async fn update_customer_payment_method( pub async fn add_card_to_locker( state: &routes::AppState, req: api::PaymentMethodCreate, - card: api::CardDetail, - customer_id: String, + card: &api::CardDetail, + customer_id: &String, merchant_account: &domain::MerchantAccount, ) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]); - request::record_operation_time( + let add_card_to_hs_resp = request::record_operation_time( async { add_card_hs( state, - req, + req.clone(), card, - customer_id, + customer_id.to_string(), merchant_account, api_enums::LockerChoice::Basilisk, None, @@ -232,7 +228,34 @@ pub async fn add_card_to_locker( &metrics::CARD_ADD_TIME, &[], ) - .await + .await?; + logger::debug!("card added to basilisk locker"); + + let add_card_to_rs_resp = request::record_operation_time( + async { + add_card_hs( + state, + req, + card, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&add_card_to_hs_resp.0.payment_method_id), + ) + .await + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + }, + &metrics::CARD_ADD_TIME, + &[], + ) + .await?; + + logger::debug!("card added to rust locker"); + + Ok(add_card_to_rs_resp) } pub async fn get_card_from_locker( @@ -243,9 +266,38 @@ pub async fn get_card_from_locker( ) -> errors::RouterResult { metrics::GET_FROM_LOCKER.add(&metrics::CONTEXT, 1, &[]); - request::record_operation_time( + let get_card_from_rs_locker_resp = request::record_operation_time( async { - get_card_from_hs_locker(state, customer_id, merchant_id, card_reference) + get_card_from_hs_locker( + state, + customer_id, + merchant_id, + card_reference, + api_enums::LockerChoice::Tartarus, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card from basilisk_hs") + .map_err(|error| { + metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + }, + &metrics::CARD_GET_TIME, + &[], + ) + .await; + + match get_card_from_rs_locker_resp { + Err(_) => request::record_operation_time( + async { + get_card_from_hs_locker( + state, + customer_id, + merchant_id, + card_reference, + api_enums::LockerChoice::Basilisk, + ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") @@ -253,11 +305,20 @@ pub async fn get_card_from_locker( metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); error }) - }, - &metrics::CARD_GET_TIME, - &[], - ) - .await + }, + &metrics::CARD_GET_TIME, + &[], + ) + .await + .map(|inner_card| { + logger::debug!("card retrieved from basilisk locker"); + inner_card + }), + Ok(_) => { + logger::debug!("card retrieved from rust locker"); + get_card_from_rs_locker_resp + } + } } pub async fn delete_card_from_locker( @@ -287,7 +348,7 @@ pub async fn delete_card_from_locker( pub async fn add_card_hs( state: &routes::AppState, req: api::PaymentMethodCreate, - card: api::CardDetail, + card: &api::CardDetail, customer_id: String, merchant_account: &domain::MerchantAccount, locker_choice: api_enums::LockerChoice, @@ -296,7 +357,7 @@ pub async fn add_card_hs( let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq { merchant_id: &merchant_account.merchant_id, merchant_customer_id: customer_id.to_owned(), - card_reference: card_reference.map(str::to_string), + requestor_card_reference: card_reference.map(str::to_string), card: payment_methods::Card { card_number: card.card_number.to_owned(), name_on_card: card.card_holder_name.to_owned(), @@ -307,11 +368,12 @@ pub async fn add_card_hs( nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(), }, }); + let store_card_payload = call_to_locker_hs(state, &payload, &customer_id, locker_choice).await?; let payment_method_resp = payment_methods::mk_add_card_response_hs( - card, + card.clone(), store_card_payload.card_reference, req, &merchant_account.merchant_id, @@ -351,6 +413,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( customer_id: &str, merchant_id: &str, payment_method_reference: &'a str, + locker_choice: Option, ) -> errors::CustomResult, errors::VaultError> { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -365,6 +428,7 @@ pub async fn get_payment_method_from_hs_locker<'a>( customer_id, merchant_id, payment_method_reference, + locker_choice, ) .await .change_context(errors::VaultError::FetchPaymentMethodFailed) @@ -466,6 +530,7 @@ pub async fn get_card_from_hs_locker<'a>( customer_id: &str, merchant_id: &str, card_reference: &'a str, + locker_choice: api_enums::LockerChoice, ) -> errors::CustomResult { let locker = &state.conf.locker; #[cfg(not(feature = "kms"))] @@ -480,6 +545,7 @@ pub async fn get_card_from_hs_locker<'a>( customer_id, merchant_id, card_reference, + Some(locker_choice), ) .await .change_context(errors::VaultError::FetchCardFailed) @@ -2193,6 +2259,7 @@ pub async fn get_lookup_key_for_payout_method( &pm.customer_id, &pm.merchant_id, &pm.payment_method_id, + None, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 63a0479375e8..45182411c28c 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -28,7 +28,7 @@ pub struct StoreCardReq<'a> { pub merchant_id: &'a str, pub merchant_customer_id: String, #[serde(skip_serializing_if = "Option::is_none")] - pub card_reference: Option, + pub requestor_card_reference: Option, pub card: Card, } @@ -428,6 +428,7 @@ pub async fn mk_get_card_request_hs( customer_id: &str, merchant_id: &str, card_reference: &str, + locker_choice: Option, ) -> CustomResult { let merchant_customer_id = customer_id.to_owned(); let card_req_body = CardReqBody { @@ -448,11 +449,16 @@ pub async fn mk_get_card_request_hs( .await .change_context(errors::VaultError::RequestEncodingFailed)?; - let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + + let jwe_payload = mk_basilisk_req(jwekey, &jws, target_locker).await?; let body = utils::Encode::::encode_to_value(&jwe_payload) .change_context(errors::VaultError::RequestEncodingFailed)?; - let mut url = locker.host.to_owned(); + let mut url = match target_locker { + api_enums::LockerChoice::Basilisk => locker.host.to_owned(), + api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), + }; url.push_str("/cards/retrieve"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 794180e2112e..551d1c8abb9a 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -183,8 +183,8 @@ pub async fn save_in_locker( Some(card) => payment_methods::cards::add_card_to_locker( state, payment_method_request, - card, - customer_id, + &card, + &customer_id, merchant_account, ) .await diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index c1e00b9b8000..9ddc8395738e 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -152,7 +152,7 @@ pub async fn save_payout_data_to_locker( card_isin: None, nick_name: None, }, - card_reference: None, + requestor_card_reference: None, }); ( payload, From cb88be01f22725948648976c2a5606a03b5ce92a Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Fri, 17 Nov 2023 10:04:34 +0530 Subject: [PATCH 002/443] fix(core): introduce new attempt and intent status to handle multiple partial captures (#2802) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> --- crates/common_enums/src/enums.rs | 7 +- .../stripe/payment_intents/types.rs | 6 +- .../stripe/setup_intents/types.rs | 6 +- crates/router/src/connector/utils.rs | 48 +++++++++- crates/router/src/core/payments.rs | 6 +- crates/router/src/core/payments/helpers.rs | 6 +- .../payments/operations/payment_response.rs | 23 ++--- crates/router/src/core/payments/retry.rs | 1 + crates/router/src/core/payments/types.rs | 2 +- .../src/types/storage/payment_attempt.rs | 10 -- crates/router/src/types/transformers.rs | 9 +- .../down.sql | 2 + .../up.sql | 3 + openapi/openapi_spec.json | 4 +- .../Payments - Cancel/event.test.js | 4 +- .../Payments - Capture/event.test.js | 10 ++ .../Payments - Capture/event.test.js | 10 ++ .../Payments - Retrieve-copy/event.test.js | 10 ++ .../Payments - Retrieve/event.test.js | 10 ++ .../Payments - Capture - 1/event.test.js | 10 ++ .../Payments - Capture - 2/event.test.js | 10 ++ .../Payments - Capture - 3/event.test.js | 10 ++ .../Payments - Capture/event.test.js | 6 +- .../Payments - Retrieve-copy/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Capture/request.json | 2 +- .../Payments - Retrieve-copy/event.test.js | 4 +- .../Payments - Capture/event.test.js | 4 +- .../Payments - Capture/request.json | 2 +- .../.meta.json | 7 ++ .../Payments - Capture/.event.meta.json | 3 + .../Payments - Capture/event.test.js | 94 +++++++++++++++++++ .../Payments - Capture/request.json | 39 ++++++++ .../Payments - Capture/response.json | 1 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 71 ++++++++++++++ .../Payments - Create/request.json | 88 +++++++++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++++++++ .../Payments - Retrieve/request.json | 28 ++++++ .../Payments - Retrieve/response.json | 1 + 46 files changed, 600 insertions(+), 59 deletions(-) create mode 100644 migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql create mode 100644 migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 48b0664c16d3..8b1437fa8926 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -50,6 +50,7 @@ pub enum AttemptStatus { VoidFailed, AutoRefunded, PartialCharged, + PartialChargedAndChargeable, Unresolved, #[default] Pending, @@ -68,7 +69,8 @@ impl AttemptStatus { | Self::Voided | Self::VoidFailed | Self::CaptureFailed - | Self::Failure => true, + | Self::Failure + | Self::PartialCharged => true, Self::Started | Self::AuthenticationFailed | Self::AuthenticationPending @@ -79,7 +81,7 @@ impl AttemptStatus { | Self::CodInitiated | Self::VoidInitiated | Self::CaptureInitiated - | Self::PartialCharged + | Self::PartialChargedAndChargeable | Self::Unresolved | Self::Pending | Self::PaymentMethodAwaited @@ -861,6 +863,7 @@ pub enum IntentStatus { RequiresConfirmation, RequiresCapture, PartiallyCaptured, + PartiallyCapturedAndCapturable, } #[derive( diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index c713011b80c8..3c7d5f2918f1 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -405,7 +405,9 @@ pub enum StripePaymentStatus { impl From for StripePaymentStatus { fn from(item: api_enums::IntentStatus) -> Self { match item { - api_enums::IntentStatus::Succeeded => Self::Succeeded, + api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => { + Self::Succeeded + } api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, api_enums::IntentStatus::RequiresCustomerAction @@ -413,7 +415,7 @@ impl From for StripePaymentStatus { api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => Self::RequiresCapture, + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => Self::RequiresCapture, api_enums::IntentStatus::Cancelled => Self::Canceled, } } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index dde378e55925..9d3f74af8cb8 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -313,7 +313,9 @@ pub enum StripeSetupStatus { impl From for StripeSetupStatus { fn from(item: api_enums::IntentStatus) -> Self { match item { - api_enums::IntentStatus::Succeeded => Self::Succeeded, + api_enums::IntentStatus::Succeeded | api_enums::IntentStatus::PartiallyCaptured => { + Self::Succeeded + } api_enums::IntentStatus::Failed => Self::Canceled, api_enums::IntentStatus::Processing => Self::Processing, api_enums::IntentStatus::RequiresCustomerAction => Self::RequiresAction, @@ -321,7 +323,7 @@ impl From for StripeSetupStatus { api_enums::IntentStatus::RequiresPaymentMethod => Self::RequiresPaymentMethod, api_enums::IntentStatus::RequiresConfirmation => Self::RequiresConfirmation, api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => { + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => { logger::error!("Invalid status change"); Self::Canceled } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index efabbf87aeba..9c19d4eed8f6 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -19,7 +19,10 @@ use serde::Serializer; use crate::{ consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::PaymentData, + }, pii::PeekInterface, types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId}, utils::{OptionExt, ValueExt}, @@ -74,6 +77,49 @@ pub trait RouterData { #[cfg(feature = "payouts")] fn get_quote_id(&self) -> Result; } + +pub trait PaymentResponseRouterData { + fn get_attempt_status_for_db_update( + &self, + payment_data: &PaymentData, + ) -> enums::AttemptStatus + where + F: Clone; +} + +impl PaymentResponseRouterData + for types::RouterData +where + Request: types::Capturable, +{ + fn get_attempt_status_for_db_update( + &self, + payment_data: &PaymentData, + ) -> enums::AttemptStatus + where + F: Clone, + { + match self.status { + enums::AttemptStatus::Voided => { + if payment_data.payment_intent.amount_captured > Some(0) { + enums::AttemptStatus::PartialCharged + } else { + self.status + } + } + enums::AttemptStatus::Charged => { + let captured_amount = types::Capturable::get_capture_amount(&self.request); + if Some(payment_data.payment_intent.amount) == captured_amount { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } +} + pub const SELECTED_PAYMENT_METHOD: &str = "Selected payment method"; pub fn get_unimplemented_payment_method_error_message(connector: &str) -> String { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 7e19b0b60571..000cadec0091 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1736,19 +1736,19 @@ pub fn should_call_connector( | storage_enums::IntentStatus::RequiresCustomerAction | storage_enums::IntentStatus::RequiresMerchantAction | storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ) && payment_data.force_sync.unwrap_or(false) } "PaymentCancel" => matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ), "PaymentCapture" => { matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::RequiresCapture - | storage_enums::IntentStatus::PartiallyCaptured + | storage_enums::IntentStatus::PartiallyCapturedAndCapturable ) || (matches!( payment_data.payment_intent.status, storage_enums::IntentStatus::Processing diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b9e96ec36e11..cd056f81ebb4 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1579,7 +1579,7 @@ pub(crate) fn validate_status_with_capture_method( } utils::when( status != storage_enums::IntentStatus::RequiresCapture - && status != storage_enums::IntentStatus::PartiallyCaptured + && status != storage_enums::IntentStatus::PartiallyCapturedAndCapturable && status != storage_enums::IntentStatus::Processing, || { Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { @@ -2784,6 +2784,7 @@ pub fn get_attempt_type( | enums::AttemptStatus::Pending | enums::AttemptStatus::ConfirmationAwaited | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable | enums::AttemptStatus::Voided | enums::AttemptStatus::AutoRefunded | enums::AttemptStatus::PaymentMethodAwaited @@ -2844,6 +2845,7 @@ pub fn get_attempt_type( enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture | enums::IntentStatus::PartiallyCaptured + | enums::IntentStatus::PartiallyCapturedAndCapturable | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => { Err(report!(errors::ApiErrorResponse::PreconditionFailed { @@ -3023,6 +3025,7 @@ pub fn is_manual_retry_allowed( | enums::AttemptStatus::Pending | enums::AttemptStatus::ConfirmationAwaited | enums::AttemptStatus::PartialCharged + | enums::AttemptStatus::PartialChargedAndChargeable | enums::AttemptStatus::Voided | enums::AttemptStatus::AutoRefunded | enums::AttemptStatus::PaymentMethodAwaited @@ -3042,6 +3045,7 @@ pub fn is_manual_retry_allowed( enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture | enums::IntentStatus::PartiallyCaptured + | enums::IntentStatus::PartiallyCapturedAndCapturable | enums::IntentStatus::Processing | enums::IntentStatus::Succeeded => Some(false), diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index b55b0c46f6ad..1cfc37efa449 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -11,6 +11,7 @@ use tracing_futures::Instrument; use super::{Operation, PostUpdateTracker}; use crate::{ + connector::utils::PaymentResponseRouterData, core::{ errors::{self, RouterResult, StorageErrorExt}, mandate, @@ -26,7 +27,7 @@ use crate::{ self, enums, payment_attempt::{AttemptStatusExt, PaymentAttemptExt}, }, - transformers::ForeignTryFrom, + transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, utils, @@ -389,7 +390,7 @@ async fn payment_response_update_tracker( types::PreprocessingResponseId::ConnectorTransactionId(_) => None, }; let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), payment_method_id: Some(router_data.payment_method_id), connector_metadata, preprocessing_step_id, @@ -434,7 +435,7 @@ async fn payment_response_update_tracker( utils::add_apple_pay_payment_status_metrics( router_data.status, - router_data.apple_pay_flow, + router_data.apple_pay_flow.clone(), payment_data.payment_attempt.connector.clone(), payment_data.payment_attempt.merchant_id.clone(), ); @@ -456,7 +457,7 @@ async fn payment_response_update_tracker( None => ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, @@ -504,7 +505,7 @@ async fn payment_response_update_tracker( ( None, Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { - status: router_data.status, + status: router_data.get_attempt_status_for_db_update(&payment_data), connector: None, connector_transaction_id, payment_method_id: Some(router_data.payment_method_id), @@ -610,15 +611,15 @@ async fn payment_response_update_tracker( let payment_intent_update = match &router_data.response { Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate { - status: payment_data - .payment_attempt - .get_intent_status(payment_data.payment_intent.amount_captured), + status: api_models::enums::IntentStatus::foreign_from( + payment_data.payment_attempt.status, + ), updated_by: storage_scheme.to_string(), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { - status: payment_data - .payment_attempt - .get_intent_status(payment_data.payment_intent.amount_captured), + status: api_models::enums::IntentStatus::foreign_from( + payment_data.payment_attempt.status, + ), return_url: router_data.return_url.clone(), amount_captured, updated_by: storage_scheme.to_string(), diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 376b9048c856..788e83b05e37 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -566,6 +566,7 @@ impl | storage_enums::AttemptStatus::AutoRefunded | storage_enums::AttemptStatus::CaptureFailed | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::PartialChargedAndChargeable | storage_enums::AttemptStatus::Pending | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f420a4b87a75..5e150a33d5c5 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -116,7 +116,7 @@ impl MultipleCaptureData { } let status_count_map = self.get_status_count(); if status_count_map.get(&storage_enums::CaptureStatus::Charged) > Some(&0) { - storage_enums::AttemptStatus::PartialCharged + storage_enums::AttemptStatus::PartialChargedAndChargeable } else { storage_enums::AttemptStatus::CaptureInitiated } diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index 0b415e716513..a4fbcb022005 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -16,7 +16,6 @@ pub trait PaymentAttemptExt { ) -> RouterResult; fn get_next_capture_id(&self) -> String; - fn get_intent_status(&self, amount_captured: Option) -> enums::IntentStatus; fn get_total_amount(&self) -> i64; } @@ -60,15 +59,6 @@ impl PaymentAttemptExt for PaymentAttempt { format!("{}_{}", self.attempt_id.clone(), next_sequence_number) } - fn get_intent_status(&self, amount_captured: Option) -> enums::IntentStatus { - let intent_status = enums::IntentStatus::foreign_from(self.status); - if intent_status == enums::IntentStatus::Cancelled && amount_captured > Some(0) { - enums::IntentStatus::Succeeded - } else { - intent_status - } - } - fn get_total_amount(&self) -> i64 { self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index f43abdf73ead..3ffba5aff50a 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -86,6 +86,9 @@ impl ForeignFrom for storage_enums::IntentStatus { storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction, storage_enums::AttemptStatus::PartialCharged => Self::PartiallyCaptured, + storage_enums::AttemptStatus::PartialChargedAndChargeable => { + Self::PartiallyCapturedAndCapturable + } storage_enums::AttemptStatus::Started | storage_enums::AttemptStatus::AuthenticationSuccessful | storage_enums::AttemptStatus::Authorizing @@ -135,7 +138,8 @@ impl ForeignTryFrom for storage_enums::CaptureStat | storage_enums::AttemptStatus::Unresolved | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited - | storage_enums::AttemptStatus::DeviceDataCollectionPending => { + | storage_enums::AttemptStatus::DeviceDataCollectionPending + | storage_enums::AttemptStatus::PartialChargedAndChargeable=> { Err(errors::ApiErrorResponse::PreconditionFailed { message: "AttemptStatus must be one of these for multiple partial captures [Charged, PartialCharged, Pending, CaptureInitiated, Failure, CaptureFailed]".into(), }.into()) @@ -414,7 +418,8 @@ impl ForeignFrom for Option { api_enums::IntentStatus::RequiresPaymentMethod | api_enums::IntentStatus::RequiresConfirmation | api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured => None, + | api_enums::IntentStatus::PartiallyCaptured + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => None, } } } diff --git a/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql new file mode 100644 index 000000000000..5b9acbaca48a --- /dev/null +++ b/migrations/2023-11-06-153840_introduce_new_attempt_and_intent_status/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "IntentStatus" ADD VALUE IF NOT EXISTS 'partially_captured_and_capturable'; +ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'partial_charged_and_chargeable'; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9fddde01b49a..55ff36c26ff7 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2619,6 +2619,7 @@ "void_failed", "auto_refunded", "partial_charged", + "partial_charged_and_chargeable", "unresolved", "pending", "failure", @@ -6145,7 +6146,8 @@ "requires_payment_method", "requires_confirmation", "requires_capture", - "partially_captured" + "partially_captured", + "partially_captured_and_capturable" ] }, "JCSVoucherData": { diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js index edeeb5a7b2b3..f5b74b41f5bd 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/event.test.js @@ -53,9 +53,9 @@ if (jsonData?.client_secret) { // Response body should have value "cancellation succeeded" for "payment status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'", + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js index e6f49ae73578..ea5c5df58982 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/event.test.js @@ -58,6 +58,16 @@ if (jsonData?.client_secret) { ); } +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} + // Response body should have value "connector error" for "error type" if (jsonData?.error?.type) { pm.test( diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js index e6f49ae73578..af4bbc618739 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js index 5e5839fa2934..103f31cbb80f 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.test.js @@ -33,3 +33,13 @@ if (jsonData?.payment_id) { "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js index d0a02af74367..6939cfa39d2e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.test.js @@ -59,3 +59,13 @@ if (jsonData?.client_secret) { "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js index e6f49ae73578..2c29f2cd3536 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js index e6f49ae73578..2c29f2cd3536 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured_and_capturable"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js index e6f49ae73578..2d200c507ff5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/event.test.js @@ -67,3 +67,13 @@ if (jsonData?.error?.type) { }, ); } + +// Response body should have value "cancellation succeeded" for "payment status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js index 791a3bfbc320..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } @@ -103,7 +103,7 @@ if (jsonData?.amount_capturable) { pm.test( "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", function () { - pm.expect(jsonData.amount_capturable).to.eql(6540); + pm.expect(jsonData.amount_capturable).to.eql(540); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js index 22f7c74b5db4..ae68f8b79310 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "Succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js index fc1ed092f8be..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js index cea10167ebce..c22795a2d483 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js index fc1ed092f8be..d9ade9825b6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js index cea10167ebce..c22795a2d483 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js index 8fd96aaddc5b..ee01079cab94 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/event.test.js @@ -91,9 +91,9 @@ if (jsonData?.amount) { // Response body should have value "6000" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json index 8975575ca40e..8efb99d3c905 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js index a3c023cb7ef9..0095c8cf19aa 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario26-Save card payment with manual capture/Payments - Retrieve-copy/event.test.js @@ -88,9 +88,9 @@ if (jsonData?.amount) { // Response body should have value "6000" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index 2d7dbc507fb0..b9d5ecb464b7 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -86,9 +86,9 @@ if (jsonData?.amount) { // Response body should have value "6000" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json index 9fe257ed85e6..cceb2b55f0a7 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json new file mode 100644 index 000000000000..e4ef30e39e8d --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js new file mode 100644 index 000000000000..f560d84ea730 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js @@ -0,0 +1,94 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json new file mode 100644 index 000000000000..9fe257ed85e6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json @@ -0,0 +1,39 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "capture"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json new file mode 100644 index 000000000000..0619498e38c7 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ca68dd7045be --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] From 7d05b74b950d9e078b063e17d046cbeb501d006a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 06:43:59 +0000 Subject: [PATCH 003/443] test(postman): update postman collection files --- .../checkout.postman_collection.json | 101 ++++- .../stripe.postman_collection.json | 408 +++++++++++++++++- 2 files changed, 485 insertions(+), 24 deletions(-) diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index b65320387429..2bd0ac0f26e0 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -3802,9 +3802,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -3839,7 +3839,7 @@ " pm.test(", " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.amount_capturable).to.eql(540);", " },", " );", "}", @@ -3964,9 +3964,9 @@ "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -5929,9 +5929,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6091,9 +6091,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6883,9 +6883,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -7045,9 +7045,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -9061,6 +9061,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9195,6 +9205,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9329,6 +9349,16 @@ " },", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -9916,6 +9946,16 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10042,7 +10082,16 @@ " },", " );", "}", - "" + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}" ], "type": "text/javascript" } @@ -10142,6 +10191,16 @@ " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10503,6 +10562,16 @@ " );", "}", "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "", "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", @@ -10632,9 +10701,9 @@ "// Response body should have value \"cancellation succeeded\" for \"payment status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 5d308dd0fe53..06ccae91b2c7 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -7954,9 +7954,9 @@ "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -7995,7 +7995,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -8116,9 +8116,9 @@ "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -8929,6 +8929,398 @@ } ] }, + { + "name": "Scenario4-Create payment with manual_multiple capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ @@ -10255,9 +10647,9 @@ "// Response body should have value \"6000\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", @@ -10286,7 +10678,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", From 57173860da8f9f067c8aa6bf8074420bf762ad58 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 06:44:00 +0000 Subject: [PATCH 004/443] chore(version): v1.82.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427fa7403e4c..4270442611a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.82.0 (2023-11-17) + +### Features + +- **router:** Add fallback while add card and retrieve card from rust locker ([#2888](https://github.com/juspay/hyperswitch/pull/2888)) ([`f735fb0`](https://github.com/juspay/hyperswitch/commit/f735fb0551812fd781a2db8bac5a0deef4cabb2b)) + +### Bug Fixes + +- **core:** Introduce new attempt and intent status to handle multiple partial captures ([#2802](https://github.com/juspay/hyperswitch/pull/2802)) ([`cb88be0`](https://github.com/juspay/hyperswitch/commit/cb88be01f22725948648976c2a5606a03b5ce92a)) + +### Testing + +- **postman:** Update postman collection files ([`7d05b74`](https://github.com/juspay/hyperswitch/commit/7d05b74b950d9e078b063e17d046cbeb501d006a)) + +**Full Changelog:** [`v1.81.0...v1.82.0`](https://github.com/juspay/hyperswitch/compare/v1.81.0...v1.82.0) + +- - - + + ## 1.81.0 (2023-11-16) ### Features From 375108b6df50e041fc9dbeb35a6a6b46b146037a Mon Sep 17 00:00:00 2001 From: Nitesh <126162378+nitesh-balla@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:47:21 +0530 Subject: [PATCH 005/443] docs(README): replace cloudformation deployment template with latest s3 url. (#2891) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 129a0512d4a0..bc528da9bbf5 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,7 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts 1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. -   Click here if you have not bootstrapped your region before deploying - -   +   2. Sign-in to your AWS console. From aea390a6a1c331f8e0dbea4f41218e43f7323508 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:38:23 +0530 Subject: [PATCH 006/443] feat(events): add incoming webhook payload to api events logger (#2852) Co-authored-by: Sampras lopes --- Cargo.lock | 1 + connector-template/mod.rs | 2 +- crates/common_utils/src/ext_traits.rs | 1 + crates/masking/Cargo.toml | 3 +- crates/masking/src/lib.rs | 4 +- crates/masking/src/serde.rs | 25 +++++ crates/router/Cargo.toml | 1 + crates/router/src/connector/aci.rs | 2 +- crates/router/src/connector/adyen.rs | 8 +- crates/router/src/connector/airwallex.rs | 4 +- .../src/connector/airwallex/transformers.rs | 3 +- .../router/src/connector/authorizedotnet.rs | 10 +- crates/router/src/connector/bambora.rs | 2 +- crates/router/src/connector/bankofamerica.rs | 2 +- crates/router/src/connector/bitpay.rs | 7 +- crates/router/src/connector/bluesnap.rs | 6 +- crates/router/src/connector/boku.rs | 2 +- crates/router/src/connector/braintree.rs | 8 +- crates/router/src/connector/cashtocode.rs | 7 +- crates/router/src/connector/checkout.rs | 7 +- crates/router/src/connector/coinbase.rs | 6 +- crates/router/src/connector/cryptopay.rs | 6 +- crates/router/src/connector/cybersource.rs | 2 +- crates/router/src/connector/dlocal.rs | 2 +- crates/router/src/connector/dummyconnector.rs | 2 +- crates/router/src/connector/fiserv.rs | 2 +- crates/router/src/connector/forte.rs | 2 +- crates/router/src/connector/globalpay.rs | 11 +- crates/router/src/connector/globepay.rs | 2 +- crates/router/src/connector/gocardless.rs | 21 ++-- .../src/connector/gocardless/transformers.rs | 20 ++-- crates/router/src/connector/helcim.rs | 2 +- crates/router/src/connector/iatapay.rs | 6 +- crates/router/src/connector/klarna.rs | 2 +- crates/router/src/connector/mollie.rs | 2 +- crates/router/src/connector/multisafepay.rs | 2 +- crates/router/src/connector/nexinets.rs | 2 +- crates/router/src/connector/nmi.rs | 2 +- crates/router/src/connector/noon.rs | 8 +- crates/router/src/connector/nuvei.rs | 7 +- crates/router/src/connector/opayo.rs | 2 +- crates/router/src/connector/opennode.rs | 6 +- crates/router/src/connector/payeezy.rs | 2 +- crates/router/src/connector/payme.rs | 24 ++--- crates/router/src/connector/paypal.rs | 29 ++--- crates/router/src/connector/payu.rs | 2 +- crates/router/src/connector/powertranz.rs | 2 +- crates/router/src/connector/prophetpay.rs | 2 +- crates/router/src/connector/rapyd.rs | 4 +- crates/router/src/connector/shift4.rs | 6 +- crates/router/src/connector/square.rs | 15 +-- crates/router/src/connector/stax.rs | 4 +- crates/router/src/connector/stripe.rs | 4 +- crates/router/src/connector/trustpay.rs | 8 +- crates/router/src/connector/tsys.rs | 2 +- crates/router/src/connector/volt.rs | 2 +- crates/router/src/connector/wise.rs | 2 +- crates/router/src/connector/worldline.rs | 6 +- crates/router/src/connector/worldpay.rs | 7 +- crates/router/src/connector/zen.rs | 4 +- crates/router/src/core/webhooks.rs | 102 ++++++++++++++---- crates/router/src/events/event_logger.rs | 1 + crates/router/src/routes/webhooks.rs | 3 +- crates/router/src/services/authentication.rs | 6 +- crates/router/src/types/api/webhooks.rs | 2 +- 65 files changed, 259 insertions(+), 202 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a03340093c88..730b08774fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4797,6 +4797,7 @@ dependencies = [ "digest 0.9.0", "dyn-clone", "encoding_rs", + "erased-serde", "error-stack", "euclid", "external_services", diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 7f21962109de..e441b0e5879a 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -485,7 +485,7 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/common_utils/src/ext_traits.rs b/crates/common_utils/src/ext_traits.rs index e76fe7dff5fb..d3296f989533 100644 --- a/crates/common_utils/src/ext_traits.rs +++ b/crates/common_utils/src/ext_traits.rs @@ -223,6 +223,7 @@ pub trait ByteSliceExt { } impl ByteSliceExt for [u8] { + #[track_caller] fn parse_struct<'de, T>( &'de self, type_name: &'static str, diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index 21d791642895..bf92e867dc6c 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] default = ["alloc", "serde", "diesel"] alloc = ["zeroize/alloc"] +serde = ["dep:serde", "dep:serde_json"] [package.metadata.docs.rs] all-features = true @@ -19,7 +20,7 @@ rustdoc-args = ["--cfg", "docsrs"] bytes = { version = "1", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time"], optional = true } serde = { version = "1", features = ["derive"], optional = true } -serde_json = "1.0.96" +serde_json = { version = "1.0.96", optional = true } subtle = "=2.4.1" zeroize = { version = "1.6", default-features = false } diff --git a/crates/masking/src/lib.rs b/crates/masking/src/lib.rs index d092a1b5a8b6..cb836e188428 100644 --- a/crates/masking/src/lib.rs +++ b/crates/masking/src/lib.rs @@ -42,7 +42,9 @@ mod vec; #[cfg(feature = "serde")] mod serde; #[cfg(feature = "serde")] -pub use crate::serde::{masked_serialize, Deserialize, SerializableSecret, Serialize}; +pub use crate::serde::{ + masked_serialize, Deserialize, ErasedMaskSerialize, SerializableSecret, Serialize, +}; /// This module should be included with asterisk. /// diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs index e57ed0301c2f..d1845ee29033 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -91,6 +91,31 @@ pub fn masked_serialize(value: &T) -> Result because of Rust's "object safety" rules. +/// In particular, the trait contains generic methods which cannot be made into a trait object. +/// In this case we remove the generic for assuming the serialization to be of 2 types only raw json or masked json +pub trait ErasedMaskSerialize { + /// Masked serialization. + fn masked_serialize(&self) -> Result; + /// Normal serialization. + fn raw_serialize(&self) -> Result; +} + +impl ErasedMaskSerialize for T { + fn masked_serialize(&self) -> Result { + masked_serialize(self) + } + + fn raw_serialize(&self) -> Result { + serde_json::to_value(self) + } +} + use pii_serializer::PIISerializer; mod pii_serializer { diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 01595dc18cd5..25feb373b734 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -115,6 +115,7 @@ router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +erased-serde = "0.3.31" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f6389c802f9e..f51c91f441df 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -572,7 +572,7 @@ impl api::IncomingWebhook for Aci { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index ef10fbb692fd..676f15d2f564 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -1600,17 +1600,13 @@ impl api::IncomingWebhook for Adyen { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; let response: adyen::Response = notif.into(); - let res_json = serde_json::to_value(response) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - - Ok(res_json) + Ok(Box::new(response)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index 5de7fc065e80..33e3dae72871 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -1081,13 +1081,13 @@ impl api::IncomingWebhook for Airwallex { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: airwallex::AirwallexWebhookObjectResource = request .body .parse_struct("AirwallexWebhookObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data.object) + Ok(Box::new(details.data.object)) } fn get_dispute_details( diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 031a8276bb0d..457b8d075487 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -824,7 +824,8 @@ pub enum AirwallexDisputeStage { #[derive(Debug, Deserialize)] pub struct AirwallexWebhookDataResource { - pub object: serde_json::Value, + // Should this be a secret by default since it represents webhook payload + pub object: Secret, } #[derive(Debug, Deserialize)] diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 7c3c234daecf..f3cdf0415b91 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -875,17 +875,15 @@ impl api::IncomingWebhook for Authorizedotnet { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let payload: authorizedotnet::AuthorizedotnetWebhookObjectId = request .body .parse_struct("AuthorizedotnetWebhookObjectId") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let sync_payload = serde_json::to_value( + + Ok(Box::new( authorizedotnet::AuthorizedotnetSyncResponse::try_from(payload)?, - ) - .into_report() - .change_context(errors::ConnectorError::ResponseHandlingFailed)?; - Ok(sync_payload) + )) } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index 802be26408df..ff6fdcb46769 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -685,7 +685,7 @@ impl api::IncomingWebhook for Bambora { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 51a1d722dc51..b6e19fa0d296 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -812,7 +812,7 @@ impl api::IncomingWebhook for Bankofamerica { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index dc4571b75746..856d0a9ec9d7 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt, Encode}, + utils::{self, BytesExt}, }; #[derive(Debug, Clone)] @@ -393,12 +393,11 @@ impl api::IncomingWebhook for Bitpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: BitpayWebhookDetails = request .body .parse_struct("BitpayWebhookDetails") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 7bd2ce052538..d1aa1fa25ee6 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -1119,15 +1119,13 @@ impl api::IncomingWebhook for Bluesnap { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource: bluesnap::BluesnapWebhookObjectResource = serde_urlencoded::from_bytes(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let res_json = serde_json::Value::try_from(resource)?; - - Ok(res_json) + Ok(Box::new(resource)) } } diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 7c2c1af0986b..87e8fd0eb96a 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -627,7 +627,7 @@ impl api::IncomingWebhook for Boku { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 6f5b13890367..99f6b9955d57 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -1418,17 +1418,13 @@ impl api::IncomingWebhook for Braintree { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; - let res_json = serde_json::to_value(response) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - - Ok(res_json) + Ok(Box::new(response)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 12a52e485396..a8d7d6d80504 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -391,16 +391,13 @@ impl api::IncomingWebhook for Cashtocode { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let webhook: transformers::CashtocodeIncomingWebhook = request .body .parse_struct("CashtocodeIncomingWebhook") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = - utils::Encode::::encode_to_value(&webhook) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Ok(res_json) + Ok(Box::new(webhook)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index f24c08233ed7..ca2556544f90 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -1261,7 +1261,7 @@ impl api::IncomingWebhook for Checkout { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let event_type_data: checkout::CheckoutWebhookEventTypeBody = request .body .parse_struct("CheckoutWebhookBody") @@ -1281,7 +1281,10 @@ impl api::IncomingWebhook for Checkout { utils::Encode::::encode_to_value(&payment_response) .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? }; - Ok(resource_object) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged. + + Ok(Box::new(resource_object)) } fn get_dispute_details( diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 5704ea15b005..9c0a06a52c90 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -426,12 +426,12 @@ impl api::IncomingWebhook for Coinbase { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: CoinbaseWebhookDetails = request .body .parse_struct("CoinbaseWebhookDetails") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if.event) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif.event)) } } diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index d2d8fa0f1ec2..417a36145b92 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -455,13 +455,13 @@ impl api::IncomingWebhook for Cryptopay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: CryptopayWebhookDetails = request .body .parse_struct("CryptopayWebhookDetails") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index ee6e93aebbd0..f69701f73958 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -805,7 +805,7 @@ impl api::IncomingWebhook for Cybersource { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 64d3e6f1c12f..4ae3a292fdae 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -674,7 +674,7 @@ impl api::IncomingWebhook for Dlocal { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index b501936b8713..9edcd957ff09 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -579,7 +579,7 @@ impl api::IncomingWebhook for DummyConnector { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 093f71b3da14..2bdb7177d941 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -787,7 +787,7 @@ impl api::IncomingWebhook for Fiserv { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 40448c01fabf..3aa7cee32878 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -669,7 +669,7 @@ impl api::IncomingWebhook for Forte { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index cfa1349633b2..26494d349b88 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -932,14 +932,15 @@ impl api::IncomingWebhook for Globalpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details = std::str::from_utf8(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = serde_json::from_str(details) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(res_json) + Ok(Box::new( + serde_json::from_str(details) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, + )) } } diff --git a/crates/router/src/connector/globepay.rs b/crates/router/src/connector/globepay.rs index 547bf66fb7d5..9ebea6087f42 100644 --- a/crates/router/src/connector/globepay.rs +++ b/crates/router/src/connector/globepay.rs @@ -508,7 +508,7 @@ impl api::IncomingWebhook for Globepay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/gocardless.rs b/crates/router/src/connector/gocardless.rs index 1a6ac8441652..d25357121b66 100644 --- a/crates/router/src/connector/gocardless.rs +++ b/crates/router/src/connector/gocardless.rs @@ -843,7 +843,7 @@ impl api::IncomingWebhook for Gocardless { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: gocardless::GocardlessWebhookEvent = request .body .parse_struct("GocardlessWebhookEvent") @@ -851,19 +851,14 @@ impl api::IncomingWebhook for Gocardless { let first_event = details .events .first() - .ok_or_else(|| errors::ConnectorError::WebhookReferenceIdNotFound)?; + .ok_or_else(|| errors::ConnectorError::WebhookReferenceIdNotFound)? + .clone(); match first_event.resource_type { - transformers::WebhookResourceType::Payments => serde_json::to_value( - gocardless::GocardlessPaymentsResponse::try_from(first_event)?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - transformers::WebhookResourceType::Refunds => serde_json::to_value(first_event) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - transformers::WebhookResourceType::Mandates => serde_json::to_value(first_event) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), + transformers::WebhookResourceType::Payments => Ok(Box::new( + gocardless::GocardlessPaymentsResponse::try_from(&first_event)?, + )), + transformers::WebhookResourceType::Refunds + | transformers::WebhookResourceType::Mandates => Ok(Box::new(first_event)), } } } diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index d3b2d244760f..72204b511518 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -862,14 +862,14 @@ pub struct GocardlessWebhookEvent { pub events: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct WebhookEvent { pub resource_type: WebhookResourceType, pub action: WebhookAction, pub links: WebhooksLink, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum WebhookResourceType { Payments, @@ -877,7 +877,7 @@ pub enum WebhookResourceType { Mandates, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum WebhookAction { PaymentsAction(PaymentsAction), @@ -885,7 +885,7 @@ pub enum WebhookAction { MandatesAction(MandatesAction), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PaymentsAction { Created, @@ -901,7 +901,7 @@ pub enum PaymentsAction { ResubmissionRequired, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RefundsAction { Created, @@ -912,7 +912,7 @@ pub enum RefundsAction { FundsReturned, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MandatesAction { Created, @@ -931,7 +931,7 @@ pub enum MandatesAction { Blocked, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum WebhooksLink { PaymentWebhooksLink(PaymentWebhooksLink), @@ -939,17 +939,17 @@ pub enum WebhooksLink { MandateWebhookLink(MandateWebhookLink), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RefundWebhookLink { pub refund: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaymentWebhooksLink { pub payment: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MandateWebhookLink { pub mandate: String, } diff --git a/crates/router/src/connector/helcim.rs b/crates/router/src/connector/helcim.rs index f7089bbd41b5..a1781a92ddf5 100644 --- a/crates/router/src/connector/helcim.rs +++ b/crates/router/src/connector/helcim.rs @@ -771,7 +771,7 @@ impl api::IncomingWebhook for Helcim { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 008047c1d366..ba4b95f43808 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -691,13 +691,13 @@ impl api::IncomingWebhook for Iatapay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif: IatapayPaymentsResponse = request .body .parse_struct("IatapayPaymentsResponse") .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif)) } } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 3670f65a2f02..f34414e737ff 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -520,7 +520,7 @@ impl api::IncomingWebhook for Klarna { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index ef3eb6a3e7b3..76deb0b2be88 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -582,7 +582,7 @@ impl api::IncomingWebhook for Mollie { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/multisafepay.rs b/crates/router/src/connector/multisafepay.rs index 9dc54e7b72e3..1f1099af0e71 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -523,7 +523,7 @@ impl api::IncomingWebhook for Multisafepay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/nexinets.rs b/crates/router/src/connector/nexinets.rs index f2e57792f284..a67a29d74ffe 100644 --- a/crates/router/src/connector/nexinets.rs +++ b/crates/router/src/connector/nexinets.rs @@ -682,7 +682,7 @@ impl api::IncomingWebhook for Nexinets { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index d7e9cd78bb88..eaede225d38f 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -667,7 +667,7 @@ impl api::IncomingWebhook for Nmi { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 0ea73efd94bd..b6ed231e5b50 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -744,16 +744,12 @@ impl api::IncomingWebhook for Noon { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource: noon::NoonWebhookObject = request .body .parse_struct("NoonWebhookObject") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let res_json = serde_json::to_value(noon::NoonPaymentsResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - - Ok(res_json) + Ok(Box::new(noon::NoonPaymentsResponse::from(resource))) } } diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 15702829d378..7a9f3af37f0c 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -25,7 +25,7 @@ use crate::{ storage::enums, ErrorResponse, Response, }, - utils::{self as common_utils, ByteSliceExt, Encode}, + utils::{self as common_utils, ByteSliceExt}, }; #[derive(Debug, Clone)] @@ -963,12 +963,13 @@ impl api::IncomingWebhook for Nuvei { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let body = serde_urlencoded::from_str::(&request.query_params) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let payment_response = nuvei::NuveiPaymentsResponse::from(body); - Encode::::encode_to_value(&payment_response).switch() + + Ok(Box::new(payment_response)) } } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index cc517ca1f3b8..ba0fb2046b7c 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -533,7 +533,7 @@ impl api::IncomingWebhook for Opayo { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index 3151403a5534..41d1e6c3d88c 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -420,11 +420,11 @@ impl api::IncomingWebhook for Opennode { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let notif = serde_urlencoded::from_bytes::(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Encode::::encode_to_value(¬if.status) - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + + Ok(Box::new(notif.status)) } } diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 8bb8eaa8b4c2..33a8ec65152e 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -585,7 +585,7 @@ impl api::IncomingWebhook for Payeezy { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index ef10c6d00878..1e67f8a9f350 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -1077,32 +1077,24 @@ impl api::IncomingWebhook for Payme { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let resource = serde_urlencoded::from_bytes::(request.body) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let res_json = match resource.notify_type { + match resource.notify_type { transformers::NotifyType::SaleComplete | transformers::NotifyType::SaleAuthorized | transformers::NotifyType::SaleFailure => { - serde_json::to_value(payme::PaymePaySaleResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) - } - transformers::NotifyType::Refund => { - serde_json::to_value(payme::PaymeQueryTransactionResponse::from(resource)) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) + Ok(Box::new(payme::PaymePaySaleResponse::from(resource))) } + transformers::NotifyType::Refund => Ok(Box::new( + payme::PaymeQueryTransactionResponse::from(resource), + )), transformers::NotifyType::SaleChargeback - | transformers::NotifyType::SaleChargebackRefund => serde_json::to_value(resource) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed), - }?; - - Ok(res_json) + | transformers::NotifyType::SaleChargebackRefund => Ok(Box::new(resource)), + } } fn get_dispute_details( diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index d4ab481eb9de..e514ebbed2fc 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -1189,33 +1189,24 @@ impl api::IncomingWebhook for Paypal { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: paypal::PaypalWebhooksBody = request .body .parse_struct("PaypalWebhooksBody") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - let sync_payload = match details.resource { - paypal::PaypalResource::PaypalCardWebhooks(resource) => serde_json::to_value( + Ok(match details.resource { + paypal::PaypalResource::PaypalCardWebhooks(resource) => Box::new( paypal::PaypalPaymentsSyncResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => serde_json::to_value( + ), + paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => Box::new( paypal::PaypalOrdersResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalRefundWebhooks(resource) => serde_json::to_value( + ), + paypal::PaypalResource::PaypalRefundWebhooks(resource) => Box::new( paypal::RefundSyncResponse::try_from((*resource, details.event_type))?, - ) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - paypal::PaypalResource::PaypalDisputeWebhooks(_) => serde_json::to_value(details) - .into_report() - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, - }; - Ok(sync_payload) + ), + paypal::PaypalResource::PaypalDisputeWebhooks(_) => Box::new(details), + }) } fn get_dispute_details( diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 9a8d4734f837..2868b5de0523 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -758,7 +758,7 @@ impl api::IncomingWebhook for Payu { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/powertranz.rs b/crates/router/src/connector/powertranz.rs index 04851dd1781a..d24fd27f1052 100644 --- a/crates/router/src/connector/powertranz.rs +++ b/crates/router/src/connector/powertranz.rs @@ -610,7 +610,7 @@ impl api::IncomingWebhook for Powertranz { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 6765fad2653d..e5ebe6331ba2 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -706,7 +706,7 @@ impl api::IncomingWebhook for Prophetpay { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index cd8893d0d7b1..91a538f9991b 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -900,7 +900,7 @@ impl api::IncomingWebhook for Rapyd { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let webhook: transformers::RapydIncomingWebhook = request .body .parse_struct("RapydIncomingWebhook") @@ -923,7 +923,7 @@ impl api::IncomingWebhook for Rapyd { .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? } }; - Ok(res_json) + Ok(Box::new(res_json)) } fn get_dispute_details( diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 98eb895db548..6f3a2b802014 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -815,11 +815,13 @@ impl api::IncomingWebhook for Shift4 { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: shift4::Shift4WebhookObjectResource = request .body .parse_struct("Shift4WebhookObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged + Ok(Box::new(details.data)) } } diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index 1d4d7e95dfa3..d836285755d4 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -915,24 +915,19 @@ impl api::IncomingWebhook for Square { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: square::SquareWebhookBody = request .body .parse_struct("SquareWebhookObject") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - let reference_object = match details.data.object { + Ok(match details.data.object { square::SquareWebhookObject::Payment(square_payments_response_details) => { - serde_json::to_value(square_payments_response_details) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? + Box::new(square_payments_response_details) } square::SquareWebhookObject::Refund(square_refund_response_details) => { - serde_json::to_value(square_refund_response_details) - .into_report() - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)? + Box::new(square_refund_response_details) } - }; - Ok(reference_object) + }) } } diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 0cfd2b89cd1a..024211c8caaa 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -886,10 +886,10 @@ impl api::IncomingWebhook for Stax { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let reference_object: serde_json::Value = serde_json::from_slice(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(reference_object) + Ok(Box::new(reference_object)) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 3f1263657e83..ccf843ec78d6 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -2057,13 +2057,13 @@ impl api::IncomingWebhook for Stripe { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: stripe::WebhookEventObjectResource = request .body .parse_struct("WebhookEventObjectResource") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details.data.object) + Ok(Box::new(details.data.object)) } fn get_dispute_details( &self, diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 7509131afeef..65ab5a7ba58d 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -906,16 +906,12 @@ impl api::IncomingWebhook for Trustpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details: trustpay::TrustpayWebhookResponse = request .body .parse_struct("TrustpayWebhookResponse") .switch()?; - let res_json = utils::Encode::::encode_to_value( - &details.payment_information, - ) - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(res_json) + Ok(Box::new(details.payment_information)) } fn get_webhook_source_verification_algorithm( diff --git a/crates/router/src/connector/tsys.rs b/crates/router/src/connector/tsys.rs index 71cef4be2afd..0143f5855ade 100644 --- a/crates/router/src/connector/tsys.rs +++ b/crates/router/src/connector/tsys.rs @@ -625,7 +625,7 @@ impl api::IncomingWebhook for Tsys { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 3697b8c8923f..43b6b3a3406d 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -589,7 +589,7 @@ impl api::IncomingWebhook for Volt { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index 5eba54eab4f7..865dcd5fff35 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -710,7 +710,7 @@ impl api::IncomingWebhook for Wise { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index 7fcca08d8bfe..3d928624df8f 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -808,14 +808,16 @@ impl api::IncomingWebhook for Worldline { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let details = request .body .parse_struct::("WorldlineWebhookObjectId") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? .payment .ok_or(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(details) + // Ideally this should be a strict type that has type information + // PII information is likely being logged here when this response will be logged + Ok(Box::new(details)) } fn get_webhook_api_response( diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index 60579fb5dd3e..ef01aa9a6ada 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -754,15 +754,12 @@ impl api::IncomingWebhook for Worldpay { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let body: WorldpayWebhookEventType = request .body .parse_struct("WorldpayWebhookEventType") .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; let psync_body = WorldpayEventResponse::try_from(body)?; - let res_json = serde_json::to_value(psync_body) - .into_report() - .change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?; - Ok(res_json) + Ok(Box::new(psync_body)) } } diff --git a/crates/router/src/connector/zen.rs b/crates/router/src/connector/zen.rs index bdbdf623f934..102d54bab427 100644 --- a/crates/router/src/connector/zen.rs +++ b/crates/router/src/connector/zen.rs @@ -668,11 +668,11 @@ impl api::IncomingWebhook for Zen { fn get_webhook_resource_object( &self, request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { + ) -> CustomResult, errors::ConnectorError> { let reference_object: serde_json::Value = serde_json::from_slice(request.body) .into_report() .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; - Ok(reference_object) + Ok(Box::new(reference_object)) } fn get_webhook_api_response( &self, diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index db53a3b56a15..9bbe35ba2a9d 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -1,16 +1,17 @@ pub mod types; pub mod utils; -use std::str::FromStr; +use std::{str::FromStr, time::Instant}; +use actix_web::FromRequest; use api_models::{ payments::HeaderPayload, webhooks::{self, WebhookResponseTracker}, }; -use common_utils::errors::ReportSwitchExt; +use common_utils::{errors::ReportSwitchExt, events::ApiEventsType}; use error_stack::{report, IntoReport, ResultExt}; use masking::ExposeInterface; -use router_env::{instrument, tracing}; +use router_env::{instrument, tracing, tracing_actix_web::RequestId}; use super::{errors::StorageErrorExt, metrics}; #[cfg(feature = "stripe")] @@ -24,9 +25,10 @@ use crate::{ payments, refunds, }, db::StorageInterface, + events::api_logs::ApiEvent, logger, - routes::{lock_utils, metrics::request::add_attributes, AppState}, - services, + routes::{app::AppStateInfo, lock_utils, metrics::request::add_attributes, AppState}, + services::{self, authentication as auth}, types::{ self as router_types, api::{self, mandates::MandateResponseExt}, @@ -860,6 +862,7 @@ pub async fn trigger_webhook_to_merchant( } pub async fn webhooks_wrapper( + flow: &impl router_env::types::FlowMetric, state: AppState, req: &actix_web::HttpRequest, merchant_account: domain::MerchantAccount, @@ -867,21 +870,64 @@ pub async fn webhooks_wrapper RouterResponse { - let (application_response, _webhooks_response_tracker) = Box::pin(webhooks_core::( - state, - req, - merchant_account, - key_store, - connector_name_or_mca_id, - body, - )) - .await?; + let start_instant = Instant::now(); + let (application_response, webhooks_response_tracker, serialized_req) = + Box::pin(webhooks_core::( + state.clone(), + req, + merchant_account.clone(), + key_store, + connector_name_or_mca_id, + body.clone(), + )) + .await?; + let request_duration = Instant::now() + .saturating_duration_since(start_instant) + .as_millis(); + + let request_id = RequestId::extract(req) + .await + .into_report() + .attach_printable("Unable to extract request id from request") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let auth_type = auth::AuthenticationType::WebhookAuth { + merchant_id: merchant_account.merchant_id.clone(), + }; + let status_code = 200; + let api_event = ApiEventsType::Webhooks { + connector: connector_name_or_mca_id.to_string(), + payment_id: webhooks_response_tracker.get_payment_id(), + }; + let response_value = serde_json::to_value(&webhooks_response_tracker) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not convert webhook effect to string")?; + + let api_event = ApiEvent::new( + flow, + &request_id, + request_duration, + status_code, + serialized_req, + Some(response_value), + None, + auth_type, + api_event, + req, + ); + match api_event.clone().try_into() { + Ok(event) => { + state.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, event=?api_event, "Error Logging API Event"); + } + } Ok(application_response) } #[instrument(skip_all)] - pub async fn webhooks_core( state: AppState, req: &actix_web::HttpRequest, @@ -892,6 +938,7 @@ pub async fn webhooks_core errors::RouterResult<( services::ApplicationResponse, WebhookResponseTracker, + serde_json::Value, )> { metrics::WEBHOOK_INCOMING_COUNT.add( &metrics::CONTEXT, @@ -973,7 +1020,11 @@ pub async fn webhooks_core = Box::new(serde_json::Value::Null); let webhook_effect = if process_webhook_further && !matches!(flow_type, api::WebhookFlow::ReturnResponse) { @@ -1072,14 +1124,21 @@ pub async fn webhooks_core::encode_to_vec(&event_object) + resource_object: event_object + .raw_serialize() + .and_then(|ref val| serde_json::to_vec(val)) + .into_report() + .change_context(errors::ParsingError::EncodeError("byte-vec")) + .attach_printable_lazy(|| { + "Unable to convert webhook paylaod to a value".to_string() + }) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( "There was an issue when encoding the incoming webhook body to bytes", @@ -1184,7 +1243,12 @@ pub async fn webhooks_core( let (merchant_id, connector_id_or_name) = path.into_inner(); Box::pin(api::server_wrap( - flow, + flow.clone(), state, &req, (), |state, auth, _| { webhooks::webhooks_wrapper::( + &flow, state.to_owned(), &req, auth.merchant_account, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index da4dec2eec8a..4277205b0231 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -54,6 +54,9 @@ pub enum AuthenticationType { PublishableKey { merchant_id: String, }, + WebhookAuth { + merchant_id: String, + }, NoAuth, } @@ -69,7 +72,8 @@ impl AuthenticationType { | Self::MerchantJWT { merchant_id, user_id: _, - } => Some(merchant_id.as_ref()), + } + | Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()), Self::AdminApiKey | Self::NoAuth => None, } } diff --git a/crates/router/src/types/api/webhooks.rs b/crates/router/src/types/api/webhooks.rs index 4bde2608c93a..52f5300d9be5 100644 --- a/crates/router/src/types/api/webhooks.rs +++ b/crates/router/src/types/api/webhooks.rs @@ -254,7 +254,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { fn get_webhook_resource_object( &self, _request: &IncomingWebhookRequestDetails<'_>, - ) -> CustomResult; + ) -> CustomResult, errors::ConnectorError>; fn get_webhook_api_response( &self, From c39beb2501e63bbf7fd41bbc947280d7ff5a71dc Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Fri, 17 Nov 2023 13:40:29 +0530 Subject: [PATCH 007/443] feat(router): Custom payment link config for payment create (#2741) Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Sahkal Poddar --- crates/api_models/src/admin.rs | 5 +++ crates/api_models/src/payments.rs | 2 ++ crates/diesel_models/src/payment_link.rs | 5 ++- crates/diesel_models/src/schema.rs | 1 + crates/router/src/core/payment_link.rs | 32 ++++++++++++------- .../payments/operations/payment_create.rs | 6 ++++ .../down.sql | 2 ++ .../up.sql | 2 ++ openapi/openapi_spec.json | 10 +++++- 9 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql create mode 100644 migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 979214a071a9..6b9928734cef 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -455,6 +455,11 @@ pub struct PrimaryBusinessDetails { #[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PaymentLinkConfig { + #[schema( + max_length = 255, + max_length = 255, + example = "https://i.imgur.com/RfxPFQo.png" + )] pub merchant_logo: Option, pub color_scheme: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index d924fb2e4f62..b479f4442ba6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3150,6 +3150,8 @@ pub struct PaymentLinkObject { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, pub merchant_custom_domain_name: Option, + #[schema(value_type = PaymentLinkConfig)] + pub payment_link_config: Option, /// Custom merchant name for payment link pub custom_merchant_name: Option, } diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 50cc5e89cee9..264cc915b35a 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -4,7 +4,7 @@ use time::PrimitiveDateTime; use crate::{enums as storage_enums, schema::payment_link}; -#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[derive(Clone, Debug, Identifiable, Queryable, Serialize, Deserialize)] #[diesel(table_name = payment_link)] #[diesel(primary_key(payment_link_id))] pub struct PaymentLink { @@ -21,7 +21,9 @@ pub struct PaymentLink { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, } + #[derive( Clone, Debug, @@ -48,4 +50,5 @@ pub struct PaymentLinkNew { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, + pub payment_link_config: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 72d5217038c1..e9db5714bed8 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -668,6 +668,7 @@ diesel::table! { fulfilment_time -> Nullable, #[max_length = 64] custom_merchant_name -> Nullable, + payment_link_config -> Nullable, } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 2ea6a4d7f219..89d345b28674 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -3,7 +3,7 @@ use common_utils::{ consts::{ DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, }, - ext_traits::ValueExt, + ext_traits::{OptionExt, ValueExt}, }; use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; @@ -15,7 +15,6 @@ use crate::{ routes::AppState, services, types::{domain, storage::enums as storage_enums, transformers::ForeignFrom}, - utils::OptionExt, }; pub async fn retrieve_payment_link( @@ -71,16 +70,11 @@ pub async fn intiate_payment_link_flow( .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let payment_link_config = merchant_account - .payment_link_config - .map(|pl_config| { - serde_json::from_value::(pl_config) - .into_report() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config", - }) - }) - .transpose()?; + let payment_link_config = if let Some(pl_config) = payment_link.payment_link_config.clone() { + extract_payment_link_config(Some(pl_config))? + } else { + extract_payment_link_config(merchant_account.payment_link_config.clone())? + }; let order_details = validate_order_details(payment_intent.order_details)?; @@ -235,3 +229,17 @@ fn validate_order_details( }); Ok(updated_order_details) } + +fn extract_payment_link_config( + pl_config: Option, +) -> Result, error_stack::Report> { + pl_config + .map(|config| { + serde_json::from_value::(config) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose() +} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 974f5e6ab5b6..1fd4c7014c35 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -800,6 +800,11 @@ async fn create_payment_link( merchant_id.clone(), payment_id.clone() ); + + let payment_link_config = payment_link_object.payment_link_config.map(|pl_config|{ + common_utils::ext_traits::Encode::::encode_to_value(&pl_config) + }).transpose().change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "payment_link_config" })?; + let payment_link_req = storage::PaymentLinkNew { payment_link_id: payment_link_id.clone(), payment_id: payment_id.clone(), @@ -810,6 +815,7 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + payment_link_config, custom_merchant_name: payment_link_object.custom_merchant_name, }; let payment_link_db = db diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql new file mode 100644 index 000000000000..b5ffba726937 --- /dev/null +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; \ No newline at end of file diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql new file mode 100644 index 000000000000..8940273ecd25 --- /dev/null +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS payment_link_config JSONB NULL; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 55ff36c26ff7..be66a1bff92c 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8221,7 +8221,9 @@ "properties": { "merchant_logo": { "type": "string", - "nullable": true + "example": "https://i.imgur.com/RfxPFQo.png", + "nullable": true, + "maxLength": 255 }, "color_scheme": { "allOf": [ @@ -8250,6 +8252,9 @@ }, "PaymentLinkObject": { "type": "object", + "required": [ + "payment_link_config" + ], "properties": { "link_expiry": { "type": "string", @@ -8260,6 +8265,9 @@ "type": "string", "nullable": true }, + "payment_link_config": { + "$ref": "#/components/schemas/PaymentLinkConfig" + }, "custom_merchant_name": { "type": "string", "description": "Custom merchant name for payment link", From 9a201ae698c2cf52e617660f82d5bf1df2e797ae Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:48:42 +0530 Subject: [PATCH 008/443] fix(router): add rust locker url in proxy_bypass_urls (#2902) --- crates/router/src/services/api/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 8eb6ab72f988..cc7353dcda6b 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -110,11 +110,15 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); + let locker_host_rs = locker.host_rs.to_owned(); let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), format!("{locker_host}/cards/delete"), + format!("{locker_host_rs}/cards/add"), + format!("{locker_host_rs}/cards/retrieve"), + format!("{locker_host_rs}/cards/delete"), format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), From 1d48a83c485c27cced7ce1441060333bbb54dbc7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:21:10 +0000 Subject: [PATCH 009/443] chore(version): v1.83.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4270442611a8..d5d04d15669e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.83.0 (2023-11-17) + +### Features + +- **events:** Add incoming webhook payload to api events logger ([#2852](https://github.com/juspay/hyperswitch/pull/2852)) ([`aea390a`](https://github.com/juspay/hyperswitch/commit/aea390a6a1c331f8e0dbea4f41218e43f7323508)) +- **router:** Custom payment link config for payment create ([#2741](https://github.com/juspay/hyperswitch/pull/2741)) ([`c39beb2`](https://github.com/juspay/hyperswitch/commit/c39beb2501e63bbf7fd41bbc947280d7ff5a71dc)) + +### Bug Fixes + +- **router:** Add rust locker url in proxy_bypass_urls ([#2902](https://github.com/juspay/hyperswitch/pull/2902)) ([`9a201ae`](https://github.com/juspay/hyperswitch/commit/9a201ae698c2cf52e617660f82d5bf1df2e797ae)) + +### Documentation + +- **README:** Replace cloudformation deployment template with latest s3 url. ([#2891](https://github.com/juspay/hyperswitch/pull/2891)) ([`375108b`](https://github.com/juspay/hyperswitch/commit/375108b6df50e041fc9dbeb35a6a6b46b146037a)) + +**Full Changelog:** [`v1.82.0...v1.83.0`](https://github.com/juspay/hyperswitch/compare/v1.82.0...v1.83.0) + +- - - + + ## 1.82.0 (2023-11-17) ### Features From 606daa9367cac8c2ea926313019deab2f938b591 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:19:41 +0530 Subject: [PATCH 010/443] fix(router): add choice to use the appropriate key for jws verification (#2917) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../router/src/core/payment_methods/cards.rs | 39 +++++++++++-------- .../src/core/payment_methods/transformers.rs | 17 +++++++- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 4ab7d334f883..80daf66a6926 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -440,10 +440,11 @@ pub async fn get_payment_method_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchPaymentMethodFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, locker_choice) + .await + .change_context(errors::VaultError::FetchPaymentMethodFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchPaymentMethodFailed)?; @@ -490,10 +491,11 @@ pub async fn call_to_locker_hs<'a>( .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::SaveCardFailed) - .attach_printable("Error getting decrypted response payload")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::SaveCardFailed) + .attach_printable("Error getting decrypted response payload")?; let stored_card_resp: payment_methods::StoreCardResp = decrypted_payload .parse_struct("StoreCardResp") .change_context(errors::VaultError::ResponseDeserializationFailed)?; @@ -557,10 +559,11 @@ pub async fn get_card_from_hs_locker<'a>( let jwe_body: services::JweBody = response .get_response_inner("JweBody") .change_context(errors::VaultError::FetchCardFailed)?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::VaultError::FetchCardFailed) - .attach_printable("Error getting decrypted response payload for get card")?; + let decrypted_payload = + payment_methods::get_decrypted_response_payload(jwekey, jwe_body, Some(locker_choice)) + .await + .change_context(errors::VaultError::FetchCardFailed) + .attach_printable("Error getting decrypted response payload for get card")?; let get_card_resp: payment_methods::RetrieveCardResp = decrypted_payload .parse_struct("RetrieveCardResp") .change_context(errors::VaultError::FetchCardFailed)?; @@ -609,10 +612,14 @@ pub async fn delete_card_from_hs_locker<'a>( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while executing call_connector_api for delete card"); let jwe_body: services::JweBody = response.get_response_inner("JweBody")?; - let decrypted_payload = payment_methods::get_decrypted_response_payload(jwekey, jwe_body) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting decrypted response payload for delete card")?; + let decrypted_payload = payment_methods::get_decrypted_response_payload( + jwekey, + jwe_body, + Some(api_enums::LockerChoice::Basilisk), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error getting decrypted response payload for delete card")?; let delete_card_resp: payment_methods::DeleteCardResp = decrypted_payload .parse_struct("DeleteCardResp") .change_context(errors::ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 45182411c28c..3b4d057e6025 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -189,14 +189,27 @@ pub async fn get_decrypted_response_payload( #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, jwe_body: encryption::JweBody, + locker_choice: Option, ) -> CustomResult { + let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); + #[cfg(feature = "kms")] - let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => { + jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() + } + }; + #[cfg(feature = "kms")] let private_key = jwekey.jwekey.peek().vault_private_key.as_bytes(); #[cfg(not(feature = "kms"))] - let public_key = jwekey.vault_encryption_key.as_bytes(); + let public_key = match target_locker { + api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), + api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), + }; + #[cfg(not(feature = "kms"))] let private_key = jwekey.vault_private_key.as_bytes(); From 0a88336b443e240d16837c43f6edd59455ad4cb6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:51:54 +0000 Subject: [PATCH 011/443] chore(version): v1.83.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d04d15669e..021a0326f025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.83.1 (2023-11-17) + +### Bug Fixes + +- **router:** Add choice to use the appropriate key for jws verification ([#2917](https://github.com/juspay/hyperswitch/pull/2917)) ([`606daa9`](https://github.com/juspay/hyperswitch/commit/606daa9367cac8c2ea926313019deab2f938b591)) + +**Full Changelog:** [`v1.83.0...v1.83.1`](https://github.com/juspay/hyperswitch/compare/v1.83.0...v1.83.1) + +- - - + + ## 1.83.0 (2023-11-17) ### Features From bdcc138e8d84577fc99f9a9aef3484b66f98209a Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 17 Nov 2023 21:38:52 +0530 Subject: [PATCH 012/443] feat(connector): [BANKOFAMERICA] PSYNC Bugfix (#2897) --- .../connector/bankofamerica/transformers.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 20b2af48b168..a6fa8652b27d 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -273,7 +273,8 @@ pub enum BankofamericaPaymentStatus { impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { fn foreign_from((status, auto_capture): (BankofamericaPaymentStatus, bool)) -> Self { match status { - BankofamericaPaymentStatus::Authorized => { + BankofamericaPaymentStatus::Authorized + | BankofamericaPaymentStatus::AuthorizedPendingReview => { if auto_capture { // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment Self::Pending @@ -281,7 +282,6 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { Self::Authorized } } - BankofamericaPaymentStatus::AuthorizedPendingReview => Self::Authorized, BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { Self::Charged } @@ -321,7 +321,7 @@ pub struct BankOfAmericaErrorInformationResponse { #[derive(Debug, Deserialize)] pub struct BankOfAmericaErrorInformation { reason: Option, - message: String, + message: Option, } impl @@ -369,7 +369,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, @@ -422,7 +425,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, @@ -475,7 +481,10 @@ impl BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error_response.error_information.message, + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, From 94897d841e25d0be8debdfe1ec674f28848e2ad4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:57:32 +0000 Subject: [PATCH 013/443] chore(version): v1.84.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 021a0326f025..141bfd40ac5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.84.0 (2023-11-17) + +### Features + +- **connector:** [BANKOFAMERICA] PSYNC Bugfix ([#2897](https://github.com/juspay/hyperswitch/pull/2897)) ([`bdcc138`](https://github.com/juspay/hyperswitch/commit/bdcc138e8d84577fc99f9a9aef3484b66f98209a)) + +**Full Changelog:** [`v1.83.1...v1.84.0`](https://github.com/juspay/hyperswitch/compare/v1.83.1...v1.84.0) + +- - - + + ## 1.83.1 (2023-11-17) ### Bug Fixes From 25cef386b8876b43893f20b93cd68ece6e68412d Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:54:55 +0530 Subject: [PATCH 014/443] feat(mca): Add new `auth_type` and a status field for mca (#2883) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/admin.rs | 9 +++ crates/common_enums/src/enums.rs | 22 +++++++ crates/diesel_models/src/enums.rs | 7 ++- .../src/merchant_connector_account.rs | 4 ++ crates/diesel_models/src/schema.rs | 1 + crates/kgraph_utils/benches/evaluation.rs | 1 + crates/kgraph_utils/src/mca.rs | 1 + .../src/connector/square/transformers.rs | 1 + crates/router/src/core/admin.rs | 62 ++++++++++++++++++- crates/router/src/core/verification/utils.rs | 1 + .../src/db/merchant_connector_account.rs | 2 + crates/router/src/openapi.rs | 1 + crates/router/src/types.rs | 1 + .../domain/merchant_connector_account.rs | 7 +++ crates/router/src/types/transformers.rs | 1 + .../down.sql | 3 + .../up.sql | 11 ++++ openapi/openapi_spec.json | 25 +++++++- 18 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 migrations/2023-11-12-131143_connector-status-column/down.sql create mode 100644 migrations/2023-11-12-131143_connector-status-column/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 6b9928734cef..efde4a048323 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -609,6 +609,9 @@ pub struct MerchantConnectorCreate { pub profile_id: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -714,6 +717,9 @@ pub struct MerchantConnectorResponse { pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: api_enums::ConnectorStatus, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." @@ -788,6 +794,9 @@ pub struct MerchantConnectorUpdate { pub connector_webhook_details: Option, pub pm_auth_config: Option, + + #[schema(value_type = ConnectorStatus, example = "inactive")] + pub status: Option, } ///Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 8b1437fa8926..cf3c398f8f48 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1857,3 +1857,25 @@ pub enum ApplePayFlow { Simplified, Manual, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + serde::Deserialize, + serde::Serialize, + ToSchema, + Default, +)] +#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ConnectorStatus { + #[default] + Inactive, + Active, +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index ec021f0f51a5..817fee633190 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -3,9 +3,10 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, - DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, - DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, + DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, + DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbMandateType as MandateType, diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index a4faa45ce4bc..e45ef0026261 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -42,6 +42,7 @@ pub struct MerchantConnectorAccount { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -70,6 +71,7 @@ pub struct MerchantConnectorAccountNew { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: storage_enums::ConnectorStatus, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -93,6 +95,7 @@ pub struct MerchantConnectorAccountUpdateInternal { #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: Option, } impl MerchantConnectorAccountUpdateInternal { @@ -115,6 +118,7 @@ impl MerchantConnectorAccountUpdateInternal { frm_config: self.frm_config, modified_at: self.modified_at.unwrap_or(source.modified_at), pm_auth_config: self.pm_auth_config, + status: self.status.unwrap_or(source.status), ..source } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e9db5714bed8..190a123185e4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -492,6 +492,7 @@ diesel::table! { profile_id -> Nullable, applepay_verified_domains -> Nullable>>, pm_auth_config -> Nullable, + status -> ConnectorStatus, } } diff --git a/crates/kgraph_utils/benches/evaluation.rs b/crates/kgraph_utils/benches/evaluation.rs index ecea12203f8a..6105dc85d7e6 100644 --- a/crates/kgraph_utils/benches/evaluation.rs +++ b/crates/kgraph_utils/benches/evaluation.rs @@ -65,6 +65,7 @@ fn build_test_data<'a>(total_enabled: usize, total_pm_types: usize) -> graph::Kn profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; kgraph_utils::mca::make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index 34babd7a02bd..deea51bd8808 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -410,6 +410,7 @@ mod tests { profile_id: None, applepay_verified_domains: None, pm_auth_config: None, + status: api_enums::ConnectorStatus::Inactive, }; make_mca_graph(vec![stripe_account]).expect("Failed graph construction") diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 54a7c461dbfc..dfb49e8e6775 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -334,6 +334,7 @@ impl TryFrom<&types::ConnectorAuthType> for SquareAuthType { | types::ConnectorAuthType::SignatureKey { .. } | types::ConnectorAuthType::MultiAuthKey { .. } | types::ConnectorAuthType::CurrencyAuthKey { .. } + | types::ConnectorAuthType::TemporaryAuth { .. } | types::ConnectorAuthType::NoKey { .. } => { Err(errors::ConnectorError::FailedToObtainAuthType.into()) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 39b4749535b7..c921a9164cb0 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -868,6 +868,15 @@ pub async fn create_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error updating the merchant account when creating payment connector")?; + let (connector_status, disabled) = validate_status_and_disabled( + req.status, + req.disabled, + auth, + // The validate_status_and_disabled function will use this value only + // when the status can be active. So we are passing this as fallback. + api_enums::ConnectorStatus::Active, + )?; + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -886,7 +895,7 @@ pub async fn create_payment_connector( .attach_printable("Unable to encrypt connector account details")?, payment_methods_enabled, test_mode: req.test_mode, - disabled: req.disabled, + disabled, metadata: req.metadata, frm_configs, connector_label: Some(connector_label), @@ -911,6 +920,7 @@ pub async fn create_payment_connector( profile_id: Some(profile_id.clone()), applepay_verified_domains: None, pm_auth_config: req.pm_auth_config.clone(), + status: connector_status, }; let mut default_routing_config = @@ -1083,6 +1093,19 @@ pub async fn update_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + let auth: types::ConnectorAuthType = req + .connector_account_details + .clone() + .unwrap_or(mca.connector_account_details.clone().into_inner()) + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "connector_account_details".to_string(), + expected_format: "auth_type and api_key".to_string(), + })?; + + let (connector_status, disabled) = + validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), @@ -1098,7 +1121,7 @@ pub async fn update_payment_connector( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting data")?, test_mode: req.test_mode, - disabled: req.disabled, + disabled, payment_methods_enabled, metadata: req.metadata, frm_configs, @@ -1115,6 +1138,7 @@ pub async fn update_payment_connector( }, applepay_verified_domains: None, pm_auth_config: req.pm_auth_config, + status: Some(connector_status), }; let updated_mca = db @@ -1722,3 +1746,37 @@ pub async fn validate_dummy_connector_enabled( Ok(()) } } + +pub fn validate_status_and_disabled( + status: Option, + disabled: Option, + auth: types::ConnectorAuthType, + current_status: api_enums::ConnectorStatus, +) -> RouterResult<(api_enums::ConnectorStatus, Option)> { + let connector_status = match (status, auth) { + (Some(common_enums::ConnectorStatus::Active), types::ConnectorAuthType::TemporaryAuth) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector status cannot be active when using TemporaryAuth".to_string(), + } + .into()); + } + (Some(status), _) => status, + (None, types::ConnectorAuthType::TemporaryAuth) => common_enums::ConnectorStatus::Inactive, + (None, _) => current_status, + }; + + let disabled = match (disabled, connector_status) { + (Some(true), common_enums::ConnectorStatus::Inactive) => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connector cannot be enabled when connector_status is inactive or when using TemporaryAuth" + .to_string(), + } + .into()); + } + (Some(disabled), _) => Some(disabled), + (None, common_enums::ConnectorStatus::Inactive) => Some(true), + (None, _) => None, + }; + + Ok((connector_status, disabled)) +} diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 433430507fb1..56960d3cb480 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -60,6 +60,7 @@ pub async fn check_existence_and_add_domain_to_db( applepay_verified_domains: Some(already_verified_domains.clone()), pm_auth_config: None, connector_label: None, + status: None, }; state .store diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index ecf52531f28a..4fbb8f19ccff 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -643,6 +643,7 @@ impl MerchantConnectorAccountInterface for MockDb { profile_id: t.profile_id, applepay_verified_domains: t.applepay_verified_domains, pm_auth_config: t.pm_auth_config, + status: t.status, }; accounts.push(account.clone()); account @@ -839,6 +840,7 @@ mod merchant_connector_account_cache_tests { profile_id: Some(profile_id.to_string()), applepay_verified_domains: None, pm_auth_config: None, + status: common_enums::ConnectorStatus::Inactive, }; db.insert_merchant_connector_account(mca.clone(), &merchant_key) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 095e1f45f93f..04ef90546cfa 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -174,6 +174,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::AttemptStatus, api_models::enums::CaptureStatus, api_models::enums::ReconStatus, + api_models::enums::ConnectorStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7cf8f6b71fa5..ceeb93f69763 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -900,6 +900,7 @@ pub struct ResponseRouterData { #[derive(Default, Debug, Clone, serde::Deserialize)] #[serde(tag = "auth_type")] pub enum ConnectorAuthType { + TemporaryAuth, HeaderKey { api_key: Secret, }, diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 58c2e018316c..c84abbefc381 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -35,6 +35,7 @@ pub struct MerchantConnectorAccount { pub profile_id: Option, pub applepay_verified_domains: Option>, pub pm_auth_config: Option, + pub status: enums::ConnectorStatus, } #[derive(Debug)] @@ -54,6 +55,7 @@ pub enum MerchantConnectorAccountUpdate { applepay_verified_domains: Option>, pm_auth_config: Option, connector_label: Option, + status: Option, }, } @@ -89,6 +91,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }, ) } @@ -128,6 +131,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: other.profile_id, applepay_verified_domains: other.applepay_verified_domains, pm_auth_config: other.pm_auth_config, + status: other.status, }) } @@ -155,6 +159,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { profile_id: self.profile_id, applepay_verified_domains: self.applepay_verified_domains, pm_auth_config: self.pm_auth_config, + status: self.status, }) } } @@ -177,6 +182,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, } => Self { merchant_id, connector_type, @@ -194,6 +200,7 @@ impl From for MerchantConnectorAccountUpdateInte applepay_verified_domains, pm_auth_config, connector_label, + status, }, } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 3ffba5aff50a..2b7ea86cf51d 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -852,6 +852,7 @@ impl TryFrom for api_models::admin::MerchantCo profile_id: item.profile_id, applepay_verified_domains: item.applepay_verified_domains, pm_auth_config: item.pm_auth_config, + status: item.status, }) } } diff --git a/migrations/2023-11-12-131143_connector-status-column/down.sql b/migrations/2023-11-12-131143_connector-status-column/down.sql new file mode 100644 index 000000000000..9463f4d77135 --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS status; +DROP TYPE IF EXISTS "ConnectorStatus"; diff --git a/migrations/2023-11-12-131143_connector-status-column/up.sql b/migrations/2023-11-12-131143_connector-status-column/up.sql new file mode 100644 index 000000000000..7a992d142d6f --- /dev/null +++ b/migrations/2023-11-12-131143_connector-status-column/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here +CREATE TYPE "ConnectorStatus" AS ENUM ('active', 'inactive'); + +ALTER TABLE merchant_connector_account +ADD COLUMN status "ConnectorStatus"; + +UPDATE merchant_connector_account SET status='active'; + +ALTER TABLE merchant_connector_account +ALTER COLUMN status SET NOT NULL, +ALTER COLUMN status SET DEFAULT 'inactive'; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index be66a1bff92c..7d94f13dd125 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4147,6 +4147,13 @@ } } }, + "ConnectorStatus": { + "type": "string", + "enum": [ + "inactive", + "active" + ] + }, "ConnectorType": { "type": "string", "enum": [ @@ -6871,7 +6878,8 @@ "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ "connector_type", - "connector_name" + "connector_name", + "status" ], "properties": { "connector_type": { @@ -7002,6 +7010,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7087,7 +7098,8 @@ "required": [ "connector_type", "connector_name", - "merchant_connector_id" + "merchant_connector_id", + "status" ], "properties": { "connector_type": { @@ -7230,6 +7242,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, @@ -7237,7 +7252,8 @@ "type": "object", "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "connector_type" + "connector_type", + "status" ], "properties": { "connector_type": { @@ -7335,6 +7351,9 @@ }, "pm_auth_config": { "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, From 644709d95f6ecaab497cf0cf3788b9e2ed88b855 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:19:02 +0530 Subject: [PATCH 015/443] fix(connector): [fiserv] fix metadata deserialization in merchant_connector_account (#2746) --- .../src/connector/fiserv/transformers.rs | 48 +++++++++++++------ crates/router/src/core/admin.rs | 1 + 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index 2d07da7f47a4..f8d88d08c6ba 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -1,4 +1,4 @@ -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii}; use error_stack::ResultExt; use serde::{Deserialize, Serialize}; @@ -150,9 +150,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP merchant_transaction_id: item.router_data.connector_request_reference_id.clone(), }; let metadata = item.router_data.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; let merchant_details = MerchantDetails { merchant_id: auth.merchant_account, @@ -230,9 +232,11 @@ impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest { fn try_from(item: &types::PaymentsCancelRouterData) -> Result { let auth: FiservAuthType = FiservAuthType::try_from(&item.connector_auth_type)?; let metadata = item.get_connector_meta()?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { merchant_details: MerchantDetails { merchant_id: auth.merchant_account, @@ -418,11 +422,21 @@ pub struct ReferenceTransactionDetails { } #[derive(Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionObject { +pub struct FiservSessionObject { pub terminal_id: String, } +impl TryFrom<&Option> for FiservSessionObject { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCaptureRequest { type Error = error_stack::Report; fn try_from( @@ -434,9 +448,11 @@ impl TryFrom<&FiservRouterData<&types::PaymentsCaptureRouterData>> for FiservCap .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), @@ -527,9 +543,11 @@ impl TryFrom<&FiservRouterData<&types::RefundsRouterData>> for FiservRefun .connector_meta_data .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let session: SessionObject = metadata - .parse_value("SessionObject") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let session: FiservSessionObject = metadata + .parse_value("FiservSessionObject") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; Ok(Self { amount: Amount { total: item.amount.clone(), diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c921a9164cb0..3a0c938c32b4 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1589,6 +1589,7 @@ pub(crate) fn validate_auth_and_metadata_type( } api_enums::Connector::Fiserv => { fiserv::transformers::FiservAuthType::try_from(val)?; + fiserv::transformers::FiservSessionObject::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Forte => { From efeebc0f2365f0900de3dd3e10a1539621c9933d Mon Sep 17 00:00:00 2001 From: Shanks Date: Mon, 20 Nov 2023 16:12:06 +0530 Subject: [PATCH 016/443] fix(router): associate parent payment token with `payment_method_id` as hyperswitch token for saved cards (#2130) Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> --- crates/router/src/core/errors.rs | 2 +- crates/router/src/core/payment_methods.rs | 77 ++++++- .../router/src/core/payment_methods/cards.rs | 77 ++++--- crates/router/src/core/payments/helpers.rs | 213 ++++++++++++------ crates/router/src/routes/payment_methods.rs | 13 +- .../src/types/storage/payment_method.rs | 40 ++++ 6 files changed, 320 insertions(+), 102 deletions(-) diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 810c079987eb..03bb9a41b5b5 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -19,7 +19,7 @@ use storage_impl::errors as storage_impl_errors; pub use user::*; pub use self::{ - api_error_response::ApiErrorResponse, + api_error_response::{ApiErrorResponse, NotImplementedMessage}, customers_error_response::CustomersErrorResponse, sch_errors::*, storage_errors::*, diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index b19b381af507..0628d301796e 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -9,13 +9,17 @@ pub use api_models::{ pub use common_utils::request::RequestBody; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; +use error_stack::IntoReport; use crate::{ - core::{errors::RouterResult, payments::helpers}, + core::{ + errors::{self, RouterResult}, + payments::helpers, + }, routes::AppState, types::{ api::{self, payments}, - domain, + domain, storage, }, }; @@ -30,6 +34,14 @@ pub trait PaymentMethodRetrieve { payment_attempt: &PaymentAttempt, merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult<(Option, Option)>; + + async fn retrieve_payment_method_with_token( + state: &AppState, + key_store: &domain::MerchantKeyStore, + token: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult>; } #[async_trait::async_trait] @@ -105,4 +117,65 @@ impl PaymentMethodRetrieve for Oss { _ => Ok((None, None)), } } + + async fn retrieve_payment_method_with_token( + state: &AppState, + merchant_key_store: &domain::MerchantKeyStore, + token_data: &storage::PaymentTokenData, + payment_intent: &PaymentIntent, + card_cvc: Option>, + ) -> RouterResult> { + match token_data { + storage::PaymentTokenData::TemporaryGeneric(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + ) + .await + } + + storage::PaymentTokenData::Temporary(generic_token) => { + helpers::retrieve_payment_method_with_temporary_token( + state, + &generic_token.token, + payment_intent, + card_cvc, + merchant_key_store, + ) + .await + } + + storage::PaymentTokenData::Permanent(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::PermanentCard(card_token) => { + helpers::retrieve_card_with_permanent_token( + state, + &card_token.token, + payment_intent, + card_cvc, + ) + .await + .map(|card| Some((card, enums::PaymentMethod::Card))) + } + + storage::PaymentTokenData::AuthBankDebit(_) => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::NotImplementedMessage::Default, + }) + .into_report() + } + } + } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 80daf66a6926..f2eeedf5388f 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -50,7 +50,7 @@ use crate::{ self, types::{decrypt, encrypt_optional, AsyncLift}, }, - storage::{self, enums}, + storage::{self, enums, PaymentTokenData}, transformers::ForeignFrom, }, utils::{self, ConnectorResponseExt, OptionExt}, @@ -2103,23 +2103,32 @@ pub async fn list_customer_payment_method( let mut customer_pms = Vec::new(); for pm in resp.into_iter() { let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); - let hyperswitch_token = generate_id(consts::ID_LENGTH, "token"); - let card = if pm.payment_method == enums::PaymentMethod::Card { - get_card_details(&pm, key, state, &hyperswitch_token, &key_store).await? - } else { - None - }; + let (card, pmd, hyperswitch_token_data) = match pm.payment_method { + enums::PaymentMethod::Card => ( + Some(get_card_details(&pm, key, state).await?), + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ), - #[cfg(feature = "payouts")] - let pmd = if pm.payment_method == enums::PaymentMethod::BankTransfer { - Some( - get_lookup_key_for_payout_method(state, &key_store, &hyperswitch_token, &pm) - .await?, - ) - } else { - None + #[cfg(feature = "payouts")] + enums::PaymentMethod::BankTransfer => { + let token = generate_id(consts::ID_LENGTH, "token"); + let token_data = PaymentTokenData::temporary_generic(token.clone()); + ( + None, + Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?), + token_data, + ) + } + + _ => ( + None, + None, + PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")), + ), }; + //Need validation for enabled payment method ,querying MCA let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), @@ -2134,10 +2143,7 @@ pub async fn list_customer_payment_method( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), - #[cfg(feature = "payouts")] bank_transfer: pmd, - #[cfg(not(feature = "payouts"))] - bank_transfer: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2153,7 +2159,7 @@ pub async fn list_customer_payment_method( &parent_payment_method_token, pma.payment_method, )) - .insert(intent_created, hyperswitch_token, state) + .insert(intent_created, hyperswitch_token_data, state) .await?; if let Some(metadata) = pma.metadata { @@ -2200,10 +2206,8 @@ async fn get_card_details( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, - hyperswitch_token: &str, - key_store: &domain::MerchantKeyStore, -) -> errors::RouterResult> { - let mut _card_decrypted = +) -> errors::RouterResult { + let card_decrypted = decrypt::(pm.payment_method_data.clone(), key) .await .change_context(errors::StorageError::DecryptionError) @@ -2217,16 +2221,17 @@ async fn get_card_details( _ => None, }); - Ok(Some( - get_lookup_key_from_locker(state, hyperswitch_token, pm, key_store).await?, - )) + Ok(if let Some(mut crd) = card_decrypted { + crd.scheme = pm.scheme.clone(); + crd + } else { + get_card_details_from_locker(state, pm).await? + }) } -pub async fn get_lookup_key_from_locker( +pub async fn get_card_details_from_locker( state: &routes::AppState, - payment_token: &str, pm: &storage::PaymentMethod, - merchant_key_store: &domain::MerchantKeyStore, ) -> errors::RouterResult { let card = get_card_from_locker( state, @@ -2237,9 +2242,19 @@ pub async fn get_lookup_key_from_locker( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(pm, card) + + payment_methods::get_card_detail(pm, card) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Get Card Details Failed")?; + .attach_printable("Get Card Details Failed") +} + +pub async fn get_lookup_key_from_locker( + state: &routes::AppState, + payment_token: &str, + pm: &storage::PaymentMethod, + merchant_key_store: &domain::MerchantKeyStore, +) -> errors::RouterResult { + let card_detail = get_card_details_from_locker(state, pm).await?; let card = card_detail.clone(); let resp = TempLockerCardSupport::create_payment_method_data_in_temp_locker( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index cd056f81ebb4..fb74006a0671 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -55,7 +55,7 @@ use crate::{ utils::{ self, crypto::{self, SignMessage}, - OptionExt, + OptionExt, StringExt, }, }; @@ -1326,6 +1326,114 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( )) } +pub async fn retrieve_payment_method_with_temporary_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, + merchant_key_store: &domain::MerchantKeyStore, +) -> RouterResult> { + let (pm, supplementary_data) = + vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) + .await + .attach_printable( + "Payment method for given token not found or there was a problem fetching it", + )?; + + utils::when( + supplementary_data + .customer_id + .ne(&payment_intent.customer_id), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + }, + )?; + + Ok::<_, error_stack::Report>(match pm { + Some(api::PaymentMethodData::Card(card)) => { + if let Some(cvc) = card_cvc { + let mut updated_card = card; + updated_card.card_cvc = cvc; + let updated_pm = api::PaymentMethodData::Card(updated_card); + vault::Vault::store_payment_method_data_in_locker( + state, + Some(token.to_owned()), + &updated_pm, + payment_intent.customer_id.to_owned(), + enums::PaymentMethod::Card, + merchant_key_store, + ) + .await?; + + Some((updated_pm, enums::PaymentMethod::Card)) + } else { + Some(( + api::PaymentMethodData::Card(card), + enums::PaymentMethod::Card, + )) + } + } + + Some(the_pm @ api::PaymentMethodData::Wallet(_)) => { + Some((the_pm, enums::PaymentMethod::Wallet)) + } + + Some(the_pm @ api::PaymentMethodData::BankTransfer(_)) => { + Some((the_pm, enums::PaymentMethod::BankTransfer)) + } + + Some(the_pm @ api::PaymentMethodData::BankRedirect(_)) => { + Some((the_pm, enums::PaymentMethod::BankRedirect)) + } + + Some(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Payment method received from locker is unsupported by locker")?, + + None => None, + }) +} + +pub async fn retrieve_card_with_permanent_token( + state: &AppState, + token: &str, + payment_intent: &PaymentIntent, + card_cvc: Option>, +) -> RouterResult { + let customer_id = payment_intent + .customer_id + .as_ref() + .get_required_value("customer_id") + .change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "no customer id provided for the payment".to_string(), + })?; + + let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; + + let api_card = api::Card { + card_number: card.card_number, + card_holder_name: card + .name_on_card + .get_required_value("name_on_card") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("card holder name was not saved in permanent locker")?, + card_exp_month: card.card_exp_month, + card_exp_year: card.card_exp_year, + card_cvc: card_cvc.unwrap_or_default(), + card_issuer: card.card_brand, + nick_name: card.nick_name.map(masking::Secret::new), + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }; + + Ok(api::PaymentMethodData::Card(api_card)) +} + pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( operation: BoxedOperation<'a, F, R, Ctx>, state: &'a AppState, @@ -1339,7 +1447,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { - Some(_) => token, + Some(_) => token.map(storage::PaymentTokenData::temporary_generic), None => { if let Some(token) = token { let redis_conn = state @@ -1358,7 +1466,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( .get_required_value("payment_method")?, ); - let key = redis_conn + let token_data_string = redis_conn .get_key::>(&key) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1369,7 +1477,26 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( }, ))?; - Some(key) + let token_data_result = token_data_string + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data"); + + let token_data = match token_data_result { + Ok(data) => data, + Err(e) => { + // The purpose of this logic is backwards compatibility to support tokens + // in redis that might be following the old format. + if token_data_string.starts_with('{') { + return Err(e); + } else { + storage::PaymentTokenData::temporary_generic(token_data_string) + } + } + }; + + Some(token_data) } else { None } @@ -1381,72 +1508,24 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { - let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker( + let payment_method_details = Ctx::retrieve_payment_method_with_token( state, - &hyperswitch_token, merchant_key_store, + &hyperswitch_token, + &payment_data.payment_intent, + card_cvc, ) .await - .attach_printable( - "Payment method for given token not found or there was a problem fetching it", - )?; + .attach_printable("in 'make_pm_data'")?; - utils::when( - supplementary_data - .customer_id - .ne(&payment_data.payment_intent.customer_id), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payment method and customer passed in payment are not same".into() }) + Ok::<_, error_stack::Report>( + if let Some((payment_method_data, payment_method)) = payment_method_details { + payment_data.payment_attempt.payment_method = Some(payment_method); + Some(payment_method_data) + } else { + None }, - )?; - - Ok::<_, error_stack::Report>(match pm.clone() { - Some(api::PaymentMethodData::Card(card)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Card); - if let Some(cvc) = card_cvc { - let mut updated_card = card; - updated_card.card_cvc = cvc; - let updated_pm = api::PaymentMethodData::Card(updated_card); - vault::Vault::store_payment_method_data_in_locker( - state, - Some(hyperswitch_token), - &updated_pm, - payment_data.payment_intent.customer_id.to_owned(), - enums::PaymentMethod::Card, - merchant_key_store, - ) - .await?; - Some(updated_pm) - } else { - pm - } - } - - Some(api::PaymentMethodData::Wallet(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::Wallet); - pm - } - - Some(api::PaymentMethodData::BankTransfer(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankTransfer); - pm - } - Some(api::PaymentMethodData::BankRedirect(_)) => { - payment_data.payment_attempt.payment_method = - Some(storage_enums::PaymentMethod::BankRedirect); - pm - } - Some(_) => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable( - "Payment method received from locker is unsupported by locker", - )?, - - None => None, - }) + ) } (Some(_), _) => { @@ -1495,7 +1574,11 @@ pub async fn store_in_vault_and_generate_ppmt( }); if let Some(key_for_hyperswitch_token) = key_for_hyperswitch_token { key_for_hyperswitch_token - .insert(Some(payment_intent.created_at), router_token, state) + .insert( + Some(payment_intent.created_at), + storage::PaymentTokenData::temporary_generic(router_token), + state, + ) .await?; }; Ok(parent_payment_method_token) diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 83d4c7f96611..43a7272a4435 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -9,7 +9,11 @@ use super::app::AppState; use crate::{ core::{api_locking, errors, payment_methods::cards}, services::{api, authentication as auth}, - types::api::payment_methods::{self, PaymentMethodId}, + types::{ + api::payment_methods::{self, PaymentMethodId}, + storage::payment_method::PaymentTokenData, + }, + utils::Encode, }; /// PaymentMethods - Create @@ -379,9 +383,12 @@ impl ParentPaymentMethodToken { pub async fn insert( &self, intent_created_at: Option, - token: String, + token: PaymentTokenData, state: &AppState, ) -> CustomResult<(), errors::ApiErrorResponse> { + let token_json_str = Encode::::encode_to_string_of_json(&token) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to serialize hyperswitch token to json")?; let redis_conn = state .store .get_redis_conn() @@ -392,7 +399,7 @@ impl ParentPaymentMethodToken { redis_conn .set_key_with_expiry( &self.key_for_token, - token, + token_json_str, TOKEN_TTL - time_elapsed.whole_seconds(), ) .await diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index 737e6f66076a..096303446dc5 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -1,4 +1,44 @@ +use api_models::payment_methods; pub use diesel_models::payment_method::{ PaymentMethod, PaymentMethodNew, PaymentMethodUpdate, PaymentMethodUpdateInternal, TokenizeCoreWorkflow, }; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PaymentTokenKind { + Temporary, + Permanent, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CardTokenData { + pub token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenericTokenData { + pub token: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PaymentTokenData { + // The variants 'Temporary' and 'Permanent' are added for backwards compatibility + // with any tokenized data present in Redis at the time of deployment of this change + Temporary(GenericTokenData), + TemporaryGeneric(GenericTokenData), + Permanent(CardTokenData), + PermanentCard(CardTokenData), + AuthBankDebit(payment_methods::BankAccountConnectorDetails), +} + +impl PaymentTokenData { + pub fn permanent_card(token: String) -> Self { + Self::PermanentCard(CardTokenData { token }) + } + + pub fn temporary_generic(token: String) -> Self { + Self::TemporaryGeneric(GenericTokenData { token }) + } +} From 39540015fde476ad8492a9142c2c1bfda8444a27 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:35:07 +0530 Subject: [PATCH 017/443] feat(router): add unified_code, unified_message in payments response (#2918) --- crates/api_models/src/gsm.rs | 6 ++ crates/api_models/src/payments.rs | 10 +++ .../src/payments/payment_attempt.rs | 8 +++ crates/diesel_models/src/gsm.rs | 12 ++++ crates/diesel_models/src/payment_attempt.rs | 20 ++++++ crates/diesel_models/src/schema.rs | 8 +++ crates/router/src/core/gsm.rs | 4 ++ crates/router/src/core/payments/helpers.rs | 45 ++++++++++++++ .../payments/operations/payment_response.rs | 17 +++++- crates/router/src/core/payments/retry.rs | 61 ++++++------------- .../router/src/core/payments/transformers.rs | 4 ++ crates/router/src/types/transformers.rs | 6 ++ crates/router/src/workflows/payment_sync.rs | 2 + .../src/mock_db/payment_attempt.rs | 2 + .../src/payments/payment_attempt.rs | 26 ++++++++ .../down.sql | 3 + .../up.sql | 3 + .../down.sql | 3 + .../up.sql | 3 + openapi/openapi_spec.json | 44 +++++++++++++ 20 files changed, 242 insertions(+), 45 deletions(-) create mode 100644 migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql create mode 100644 migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql create mode 100644 migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql create mode 100644 migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 254981b1f8f7..81798d05178b 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -13,6 +13,8 @@ pub struct GsmCreateRequest { pub router_error: Option, pub decision: GsmDecision, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -57,6 +59,8 @@ pub struct GsmUpdateRequest { pub router_error: Option, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -88,4 +92,6 @@ pub struct GsmResponse { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b479f4442ba6..9f4f151c2228 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -391,6 +391,10 @@ pub struct PaymentAttemptResponse { /// reference to the payment at connector side #[schema(value_type = Option, example = "993672945374576J")] pub reference_id: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, } #[derive( @@ -2089,6 +2093,12 @@ pub struct PaymentsResponse { #[schema(example = "Failed while verifying the card")] pub error_message: Option, + /// error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + + /// error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, + /// Payment Experience for the current payment #[schema(value_type = Option, example = "redirect_to_url")] pub payment_experience: Option, diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 88fc7b3b524a..1b43177feb56 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -145,6 +145,8 @@ pub struct PaymentAttempt { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -207,6 +209,8 @@ pub struct PaymentAttemptNew { pub authentication_data: Option, pub encoded_data: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -292,6 +296,8 @@ pub enum PaymentAttemptUpdate { updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -316,6 +322,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, }, MultipleCaptureCountUpdate { multiple_capture_count: i16, diff --git a/crates/diesel_models/src/gsm.rs b/crates/diesel_models/src/gsm.rs index 2e824758aa5a..39bd880cd6c2 100644 --- a/crates/diesel_models/src/gsm.rs +++ b/crates/diesel_models/src/gsm.rs @@ -34,6 +34,8 @@ pub struct GatewayStatusMap { #[serde(with = "custom_serde::iso8601")] pub last_modified: PrimitiveDateTime, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Insertable)] @@ -48,6 +50,8 @@ pub struct GatewayStatusMappingNew { pub router_error: Option, pub decision: String, pub step_up_possible: bool, + pub unified_code: Option, + pub unified_message: Option, } #[derive( @@ -71,6 +75,8 @@ pub struct GatewayStatusMapperUpdateInternal { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug)] @@ -79,6 +85,8 @@ pub struct GatewayStatusMappingUpdate { pub router_error: Option>, pub decision: Option, pub step_up_possible: Option, + pub unified_code: Option, + pub unified_message: Option, } impl From for GatewayStatusMapperUpdateInternal { @@ -88,12 +96,16 @@ impl From for GatewayStatusMapperUpdateInternal { status, router_error, step_up_possible, + unified_code, + unified_message, } = value; Self { status, router_error, decision, step_up_possible, + unified_code, + unified_message, ..Default::default() } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index cd976b9e19db..bb8f2b60bbb7 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -61,6 +61,8 @@ pub struct PaymentAttempt { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] @@ -124,6 +126,8 @@ pub struct PaymentAttemptNew { pub merchant_connector_id: Option, pub authentication_data: Option, pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -209,6 +213,8 @@ pub enum PaymentAttemptUpdate { updated_by: String, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, }, UnresolvedResponseUpdate { status: storage_enums::AttemptStatus, @@ -233,6 +239,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, amount_capturable: Option, updated_by: String, + unified_code: Option>, + unified_message: Option>, }, MultipleCaptureCountUpdate { multiple_capture_count: i16, @@ -298,6 +306,8 @@ pub struct PaymentAttemptUpdateInternal { merchant_connector_id: Option, authentication_data: Option, encoded_data: Option, + unified_code: Option>, + unified_message: Option>, } impl PaymentAttemptUpdate { @@ -352,6 +362,8 @@ impl PaymentAttemptUpdate { merchant_connector_id: pa_update.merchant_connector_id, authentication_data: pa_update.authentication_data.or(source.authentication_data), encoded_data: pa_update.encoded_data.or(source.encoded_data), + unified_code: pa_update.unified_code.unwrap_or(source.unified_code), + unified_message: pa_update.unified_message.unwrap_or(source.unified_message), ..source } } @@ -488,6 +500,8 @@ impl From for PaymentAttemptUpdateInternal { updated_by, authentication_data, encoded_data, + unified_code, + unified_message, } => Self { status: Some(status), connector, @@ -508,6 +522,8 @@ impl From for PaymentAttemptUpdateInternal { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::ErrorUpdate { @@ -518,6 +534,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => Self { connector, status: Some(status), @@ -527,6 +545,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 190a123185e4..ce974e409a2c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -331,6 +331,10 @@ diesel::table! { created_at -> Timestamp, last_modified -> Timestamp, step_up_possible -> Bool, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } @@ -585,6 +589,10 @@ diesel::table! { merchant_connector_id -> Nullable, authentication_data -> Nullable, encoded_data -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/router/src/core/gsm.rs b/crates/router/src/core/gsm.rs index ed72275a73ab..611a35d63632 100644 --- a/crates/router/src/core/gsm.rs +++ b/crates/router/src/core/gsm.rs @@ -65,6 +65,8 @@ pub async fn update_gsm_rule( status, router_error, step_up_possible, + unified_code, + unified_message, } = gsm_request; GsmInterface::update_gsm_rule( db, @@ -78,6 +80,8 @@ pub async fn update_gsm_rule( status, router_error: Some(router_error), step_up_possible, + unified_code, + unified_message, }, ) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index fb74006a0671..ae729ff8fa25 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3026,6 +3026,8 @@ impl AttemptType { authentication_data: None, encoded_data: None, merchant_connector_id: None, + unified_code: None, + unified_message: None, } } @@ -3516,3 +3518,46 @@ pub fn validate_payment_link_request( } Ok(()) } + +pub async fn get_gsm_record( + state: &AppState, + error_code: Option, + error_message: Option, + connector_name: String, + flow: String, +) -> Option { + let get_gsm = || async { + state.store.find_gsm_rule( + connector_name.clone(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name, + flow, + error_code, + error_message + ); + metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 1cfc37efa449..083d1bb030dd 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -16,7 +16,7 @@ use crate::{ errors::{self, RouterResult, StorageErrorExt}, mandate, payment_methods::PaymentMethodRetrieve, - payments::{types::MultipleCaptureData, PaymentData}, + payments::{helpers as payments_helpers, types::MultipleCaptureData, PaymentData}, utils as core_utils, }, routes::{metrics, AppState}, @@ -331,7 +331,16 @@ async fn payment_response_update_tracker( (Some((multiple_capture_data, capture_update_list)), None) } None => { + let connector_name = router_data.connector.to_string(); let flow_name = core_utils::get_flow_name::()?; + let option_gsm = payments_helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector_name, + flow_name.clone(), + ) + .await; let status = // mark previous attempt status for technical failures in PSync flow if flow_name == "PSync" { @@ -364,6 +373,8 @@ async fn payment_response_update_tracker( None }, updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), }), ) } @@ -470,7 +481,9 @@ async fn payment_response_update_tracker( payment_token: None, error_code: error_status.clone(), error_message: error_status.clone(), - error_reason: error_status, + error_reason: error_status.clone(), + unified_code: error_status.clone(), + unified_message: error_status, connector_response_reference_id, amount_capturable: if router_data.status.is_terminal_status() || router_data diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 788e83b05e37..3c0106206e1d 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -55,7 +55,7 @@ where metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); - let mut initial_gsm = get_gsm(state, &router_data).await; + let mut initial_gsm = get_gsm(state, &router_data).await?; //Check if step-up to threeDS is possible and merchant has enabled let step_up_possible = initial_gsm @@ -99,7 +99,7 @@ where // Use initial_gsm for first time alone let gsm = match initial_gsm.as_ref() { Some(gsm) => Some(gsm.clone()), - None => get_gsm(state, &router_data).await, + None => get_gsm(state, &router_data).await?, }; match get_gsm_decision(gsm) { @@ -214,46 +214,16 @@ pub async fn get_retries( pub async fn get_gsm( state: &app::AppState, router_data: &types::RouterData, -) -> Option { +) -> RouterResult> { let error_response = router_data.response.as_ref().err(); let error_code = error_response.map(|err| err.code.to_owned()); let error_message = error_response.map(|err| err.message.to_owned()); - let get_gsm = || async { - let connector_name = router_data.connector.to_string(); - let flow = get_flow_name::()?; - state.store.find_gsm_rule( - connector_name.clone(), - flow.clone(), - "sub_flow".to_string(), - error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response - error_message.clone().unwrap_or_default(), - ) - .await - .map_err(|err| { - if err.current_context().is_db_not_found() { - logger::warn!( - "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", - connector_name, - flow, - error_code, - error_message - ); - metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); - } else { - metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); - }; - err.change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch decision from gsm") - }) - }; - get_gsm() - .await - .map_err(|err| { - // warn log should suffice here because we are not propagating this error - logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); - err - }) - .ok() + let connector_name = router_data.connector.to_string(); + let flow = get_flow_name::()?; + Ok( + payments::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) + .await, + ) } #[instrument(skip_all)] @@ -417,6 +387,8 @@ where updated_by: storage_scheme.to_string(), authentication_data, encoded_data, + unified_code: None, + unified_message: None, }, storage_scheme, ) @@ -427,17 +399,20 @@ where logger::error!("unexpected response: this response was not expected in Retry flow"); return Ok(()); } - Err(error_response) => { + Err(ref error_response) => { + let option_gsm = get_gsm(state, &router_data).await?; db.update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), storage::PaymentAttemptUpdate::ErrorUpdate { connector: None, - error_code: Some(Some(error_response.code)), - error_message: Some(Some(error_response.message)), + error_code: Some(Some(error_response.code.clone())), + error_message: Some(Some(error_response.message.clone())), status: storage_enums::AttemptStatus::Failure, - error_reason: Some(error_response.reason), + error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), updated_by: storage_scheme.to_string(), + unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), + unified_message: option_gsm.map(|gsm| gsm.unified_message), }, storage_scheme, ) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6c6b4ae9339f..f395c023128c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -685,6 +685,8 @@ where .set_profile_id(payment_intent.profile_id) .set_attempt_count(payment_intent.attempt_count) .set_merchant_connector_id(payment_attempt.merchant_connector_id) + .set_unified_code(payment_attempt.unified_code) + .set_unified_message(payment_attempt.unified_message) .to_owned(), headers, )) @@ -745,6 +747,8 @@ where attempt_count: payment_intent.attempt_count, payment_link: payment_link_data, surcharge_details, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, ..Default::default() }, headers, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2b7ea86cf51d..b73ba0964fbf 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -878,6 +878,8 @@ impl ForeignFrom for api_models::payments::PaymentAttem payment_experience: payment_attempt.payment_experience, payment_method_type: payment_attempt.payment_method_type, reference_id: payment_attempt.connector_response_reference_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, } } } @@ -1055,6 +1057,8 @@ impl ForeignFrom for storage::GatewayStatusMapp status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } @@ -1071,6 +1075,8 @@ impl ForeignFrom for gsm_api_types::GsmResponse { status: value.status, router_error: value.router_error, step_up_possible: value.step_up_possible, + unified_code: value.unified_code, + unified_message: value.unified_message, } } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 00e7357d896f..43e327559a0c 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -136,6 +136,8 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { )), amount_capturable: Some(0), updated_by: merchant_account.storage_scheme.to_string(), + unified_code: None, + unified_message: None, }; payment_data.payment_attempt = db diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index cb2f81daa797..fe244b10325f 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -144,6 +144,8 @@ impl PaymentAttemptInterface for MockDb { authentication_data: payment_attempt.authentication_data, encoded_data: payment_attempt.encoded_data, merchant_connector_id: payment_attempt.merchant_connector_id, + unified_code: payment_attempt.unified_code, + unified_message: payment_attempt.unified_message, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 3d00e2f2bf7a..cb74c981ea71 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -364,6 +364,8 @@ impl PaymentAttemptInterface for KVRouterStore { authentication_data: payment_attempt.authentication_data.clone(), encoded_data: payment_attempt.encoded_data.clone(), merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + unified_code: payment_attempt.unified_code.clone(), + unified_message: payment_attempt.unified_message.clone(), }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -966,6 +968,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1018,6 +1022,8 @@ impl DataModelExt for PaymentAttempt { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1070,6 +1076,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: self.authentication_data, encoded_data: self.encoded_data, merchant_connector_id: self.merchant_connector_id, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -1120,6 +1128,8 @@ impl DataModelExt for PaymentAttemptNew { authentication_data: storage_model.authentication_data, encoded_data: storage_model.encoded_data, merchant_connector_id: storage_model.merchant_connector_id, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -1255,6 +1265,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -1274,6 +1286,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, Self::UnresolvedResponseUpdate { status, @@ -1307,6 +1321,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1315,6 +1331,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, }, Self::MultipleCaptureCountUpdate { multiple_capture_count, @@ -1504,6 +1522,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, } => Self::ResponseUpdate { status, connector, @@ -1523,6 +1543,8 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, authentication_data, encoded_data, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -1556,6 +1578,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, } => Self::ErrorUpdate { connector, status, @@ -1564,6 +1588,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + unified_code, + unified_message, }, DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { multiple_capture_count, diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql new file mode 100644 index 000000000000..9561c8509b69 --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_code; +ALTER TABLE gateway_status_map DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql new file mode 100644 index 000000000000..a4b1250a032a --- /dev/null +++ b/migrations/2023-11-17-061003_add-unified-error-code-mssg-gsm/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE gateway_status_map ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql new file mode 100644 index 000000000000..83609093e136 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_code; +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql new file mode 100644 index 000000000000..5e390d51f760 --- /dev/null +++ b/migrations/2023-11-17-084413_add-unified-error-code-mssg-payment-attempt/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255); +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 7d94f13dd125..65280c187142 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5934,6 +5934,14 @@ }, "step_up_possible": { "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -6039,6 +6047,14 @@ }, "step_up_possible": { "type": "boolean" + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -6113,6 +6129,14 @@ "step_up_possible": { "type": "boolean", "nullable": true + }, + "unified_code": { + "type": "string", + "nullable": true + }, + "unified_message": { + "type": "string", + "nullable": true } } }, @@ -8155,6 +8179,16 @@ "description": "reference to the payment at connector side", "example": "993672945374576J", "nullable": true + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true } } }, @@ -10041,6 +10075,16 @@ "example": "Failed while verifying the card", "nullable": true }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, "payment_experience": { "allOf": [ { From 44deeb7e7605cb5320b84c0fac1fd551877803a4 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:19:19 +0530 Subject: [PATCH 018/443] refactor(core): query business profile only once (#2830) --- crates/router/src/core/payments.rs | 64 +++--- crates/router/src/core/payments/operations.rs | 19 +- .../payments/operations/payment_approve.rs | 114 ++++++---- .../payments/operations/payment_cancel.rs | 100 +++++---- .../payments/operations/payment_capture.rs | 99 ++++---- .../operations/payment_complete_authorize.rs | 113 ++++++---- .../payments/operations/payment_confirm.rs | 212 +++++++++++------- .../payments/operations/payment_create.rs | 103 +++++---- .../payments/operations/payment_reject.rs | 103 +++++---- .../payments/operations/payment_session.rs | 99 ++++---- .../core/payments/operations/payment_start.rs | 99 ++++---- .../payments/operations/payment_status.rs | 127 ++++++----- .../payments/operations/payment_update.rs | 99 ++++---- 13 files changed, 772 insertions(+), 579 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 000cadec0091..0259c48ee827 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -31,9 +31,8 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, - PaymentMethodValidate, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, - PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, + PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, }; use self::{ flows::{ConstructFlowSpecificData, Feature}, @@ -112,7 +111,12 @@ where tracing::Span::current().record("payment_id", &format!("{}", validate_result.payment_id)); - let (operation, mut payment_data, customer_details) = operation + let operations::GetTrackerResponse { + operation, + customer_details, + mut payment_data, + business_profile, + } = operation .to_get_tracker()? .get_trackers( state, @@ -142,6 +146,7 @@ where state, &req, &merchant_account, + &business_profile, &key_store, &mut payment_data, eligible_connectors, @@ -1998,11 +2003,13 @@ where Ok(()) } +#[allow(clippy::too_many_arguments)] pub async fn get_connector_choice( operation: &BoxedOperation<'_, F, Req, Ctx>, state: &AppState, req: &Req, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, eligible_connectors: Option>, @@ -2040,6 +2047,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, Some(straight_through), @@ -2052,6 +2060,7 @@ where connector_selection( state, merchant_account, + business_profile, key_store, payment_data, None, @@ -2075,6 +2084,7 @@ where pub async fn connector_selection( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2114,6 +2124,7 @@ where let decided_connector = decide_connector( state.clone(), merchant_account, + business_profile, key_store, payment_data, request_straight_through, @@ -2141,9 +2152,11 @@ where Ok(decided_connector) } +#[allow(clippy::too_many_arguments)] pub async fn decide_connector( state: AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, request_straight_through: Option, @@ -2345,6 +2358,7 @@ where route_connector_v1( &state, merchant_account, + business_profile, key_store, payment_data, routing_data, @@ -2480,6 +2494,7 @@ where pub async fn route_connector_v1( state: &AppState, merchant_account: &domain::MerchantAccount, + business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, routing_data: &mut storage::RoutingData, @@ -2488,44 +2503,19 @@ pub async fn route_connector_v1( where F: Send + Clone, { - #[cfg(not(feature = "business_profile_routing"))] - let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) + let routing_algorithm = if cfg!(feature = "business_profile_routing") { + business_profile.routing_algorithm.clone() + } else { + merchant_account.routing_algorithm.clone() + }; + + let algorithm_ref = routing_algorithm + .map(|ra| ra.parse_value::("RoutingAlgorithmRef")) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Could not decode merchant routing algorithm ref")? .unwrap_or_default(); - #[cfg(feature = "business_profile_routing")] - let algorithm_ref: api::routing::RoutingAlgorithmRef = { - let profile_id = payment_data - .payment_intent - .profile_id - .as_ref() - .get_required_value("profile_id") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("'profile_id' not set in payment intent")?; - - let business_profile = state - .store - .find_business_profile_by_profile_id(profile_id) - .await - .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id.to_string(), - })?; - - business_profile - .routing_algorithm - .clone() - .map(|ra| ra.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not decode merchant routing algorithm ref")? - .unwrap_or_default() - }; - let connectors = routing::perform_static_routing_v1( state, &merchant_account.merchant_id, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index f65e65459e00..6f01c653084f 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -4,7 +4,6 @@ pub mod payment_capture; pub mod payment_complete_authorize; pub mod payment_confirm; pub mod payment_create; -pub mod payment_method_validate; pub mod payment_reject; pub mod payment_response; pub mod payment_session; @@ -20,10 +19,9 @@ use router_env::{instrument, tracing}; pub use self::{ payment_approve::PaymentApprove, payment_cancel::PaymentCancel, payment_capture::PaymentCapture, payment_confirm::PaymentConfirm, - payment_create::PaymentCreate, payment_method_validate::PaymentMethodValidate, - payment_reject::PaymentReject, payment_response::PaymentResponse, - payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, - payment_update::PaymentUpdate, + payment_create::PaymentCreate, payment_reject::PaymentReject, + payment_response::PaymentResponse, payment_session::PaymentSession, + payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ @@ -91,8 +89,15 @@ pub trait ValidateRequest { ) -> RouterResult<(BoxedOperation<'b, F, R, Ctx>, ValidateResult<'a>)>; } +pub struct GetTrackerResponse<'a, F: Clone, R, Ctx> { + pub operation: BoxedOperation<'a, F, R, Ctx>, + pub customer_details: Option, + pub payment_data: PaymentData, + pub business_profile: storage::business_profile::BusinessProfile, +} + #[async_trait] -pub trait GetTracker: Send { +pub trait GetTracker: Send { #[allow(clippy::too_many_arguments)] async fn get_trackers<'a>( &'a self, @@ -103,7 +108,7 @@ pub trait GetTracker: Send { merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<(BoxedOperation<'a, F, R, Ctx>, D, Option)>; + ) -> RouterResult>; } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 538e65e4b22e..78eb3fb1f10d 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -45,11 +45,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -76,6 +72,21 @@ impl "confirm", )?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let ( token, payment_method, @@ -207,50 +218,57 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 535edf736ca6..096f900e7195 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -12,7 +12,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCancelRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -128,45 +124,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ff51a2c49d77..09e79064dc69 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -41,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsCaptureRequest, Ctx>, - payments::PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -172,44 +168,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - payments::PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - force_sync: None, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + force_sync: None, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index c648d95a4950..7cc1edf17fd1 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -202,50 +198,71 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let customer_details = Some(CustomerDetails { + customer_id: request.customer_id.clone(), + name: request.name.clone(), + email: request.email.clone(), + phone: request.phone.clone(), + phone_country_code: request.phone_country_code.clone(), + }); + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index afb7f110ed5d..a040782d83cd 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -50,11 +50,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -65,7 +61,6 @@ impl .change_context(errors::ApiErrorResponse::PaymentNotFound)?; // Stage 1 - let store = state.clone().store; let m_merchant_id = merchant_id.clone(); let payment_intent_fut = tokio::spawn( @@ -137,8 +132,29 @@ impl let customer_details = helpers::get_customer_details_from_request(request); // Stage 2 - let attempt_id = payment_intent.active_attempt.get_id(); + let profile_id = payment_intent + .profile_id + .clone() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let store = state.store.clone(); + + let business_profile_fut = tokio::spawn(async move { + store + .find_business_profile_by_profile_id(&profile_id) + .map(|business_profile_result| { + business_profile_result.to_not_found_response( + errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + }, + ) + }) + .await + }); + let store = state.clone().store; let m_payment_id = payment_intent.payment_id.clone(); let m_merchant_id = merchant_id.clone(); @@ -235,48 +251,72 @@ impl .in_current_span(), ); - let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { - api_models::enums::IntentStatus::RequiresCustomerAction - | api_models::enums::IntentStatus::RequiresMerchantAction - | api_models::enums::IntentStatus::RequiresPaymentMethod - | api_models::enums::IntentStatus::RequiresConfirmation => { - let (payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( - utils::flatten_join_error(payment_attempt_fut), - utils::flatten_join_error(shipping_address_fut), - utils::flatten_join_error(billing_address_fut), - utils::flatten_join_error(config_update_fut) - )?; - - (payment_attempt, shipping_address, billing_address) - } - _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( - utils::flatten_join_error(payment_attempt_fut), - utils::flatten_join_error(shipping_address_fut), - utils::flatten_join_error(billing_address_fut), - utils::flatten_join_error(config_update_fut) - )?; - - let attempt_type = helpers::get_attempt_type( - &payment_intent, - &payment_attempt, - request, - "confirm", - )?; - - (payment_intent, payment_attempt) = attempt_type - .modify_payment_intent_and_payment_attempt( - request, - payment_intent, + // Based on whether a retry can be performed or not, fetch relevant entities + let (mut payment_attempt, shipping_address, billing_address, business_profile) = + match payment_intent.status { + api_models::enums::IntentStatus::RequiresCustomerAction + | api_models::enums::IntentStatus::RequiresMerchantAction + | api_models::enums::IntentStatus::RequiresPaymentMethod + | api_models::enums::IntentStatus::RequiresConfirmation => { + // Normal payment + let (payment_attempt, shipping_address, billing_address, business_profile, _) = + tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + ( payment_attempt, - &*state.store, - storage_scheme, + shipping_address, + billing_address, + business_profile, ) - .await?; + } + _ => { + // Retry payment + let ( + mut payment_attempt, + shipping_address, + billing_address, + business_profile, + _, + ) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(business_profile_fut), + utils::flatten_join_error(config_update_fut) + )?; + + let attempt_type = helpers::get_attempt_type( + &payment_intent, + &payment_attempt, + request, + "confirm", + )?; + + // 3 + (payment_intent, payment_attempt) = attempt_type + .modify_payment_intent_and_payment_attempt( + request, + payment_intent, + payment_attempt, + &*state.store, + storage_scheme, + ) + .await?; - (payment_attempt, shipping_address, billing_address) - } - }; + ( + payment_attempt, + shipping_address, + billing_address, + business_profile, + ) + } + }; payment_intent.order_details = request .get_order_details_as_value() @@ -382,6 +422,7 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( state, &payment_attempt, @@ -394,44 +435,49 @@ impl &payment_attempt, ); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 1fd4c7014c35..526b03137bea 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -53,11 +53,7 @@ impl merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; let merchant_id = &merchant_account.merchant_id; @@ -196,6 +192,20 @@ impl payment_id: payment_id.clone(), })?; + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let mandate_id = request .mandate_id .as_ref() @@ -246,6 +256,7 @@ impl request.confirm, self, ); + let creds_identifier = request .merchant_connector_details .as_ref() @@ -265,9 +276,8 @@ impl .transpose()?; // The operation merges mandate data from both request and payment_attempt - let setup_mandate: Option = setup_mandate.map(Into::into); + let setup_mandate = setup_mandate.map(MandateData::from); - // populate payment_data.surcharge_details from request let surcharge_details = request.surcharge_details.map(|surcharge_details| { payment_methods::SurchargeDetailsResponse { surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), @@ -280,44 +290,49 @@ impl } }); - Ok(( - operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - refunds: vec![], - disputes: vec![], - attempts: None, - force_sync: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + setup_mandate, + token, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + refunds: vec![], + disputes: vec![], + attempts: None, + force_sync: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 16d264c001ec..ae02dde4bc06 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -11,7 +11,7 @@ use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, }, routes::AppState, services, @@ -41,11 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, PaymentsRejectRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -114,45 +110,64 @@ impl format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) }); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: frm_response.ok(), - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: frm_response.ok(), + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 3abde60c2e9b..cea6eb176672 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -43,11 +43,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsSessionRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let payment_id = payment_id .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; @@ -152,44 +148,63 @@ impl .await .transpose()?; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - token: None, - setup_mandate: None, - address: payments::PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: None, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + token: None, + setup_mandate: None, + address: payments::PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 17f39d5150bb..6d4281216b4f 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -42,11 +42,7 @@ impl merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsStartRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, payment_attempt, currency, amount); let db = &*state.store; @@ -126,44 +122,63 @@ impl ..CustomerDetails::default() }; - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: None, - token: payment_attempt.payment_token.clone(), - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(payment_attempt.confirm), - payment_attempt, - payment_method_data: None, - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: payment_attempt.payment_token.clone(), + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: Some(payment_attempt.confirm), + payment_attempt, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index fb58aeb34e07..b31c406f0ecd 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -190,11 +190,8 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> + { get_tracker_for_sync( payment_id, merchant_account, @@ -221,12 +218,8 @@ async fn get_tracker_for_sync< request: &api::PaymentsRetrieveRequest, operation: Op, storage_scheme: enums::MerchantStorageScheme, -) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRetrieveRequest, Ctx>, - PaymentData, - Option, -)> { - let (payment_intent, mut payment_attempt, currency, amount); +) -> RouterResult> { + let (payment_intent, payment_attempt, currency, amount); (payment_intent, payment_attempt) = get_payment_intent_payment_attempt( db, @@ -250,7 +243,6 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); - payment_attempt.encoded_data = request.param.clone(); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -357,53 +349,74 @@ async fn get_tracker_for_sync< }) .await .transpose()?; - Ok(( - Box::new(operation), - PaymentData { - flow: PhantomData, - payment_intent, - currency, - amount, - email: None, - mandate_id: payment_attempt.mandate_id.clone().map(|id| { - api_models::payments::MandateIds { - mandate_id: id, - mandate_reference_id: None, - } + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + currency, + amount, + email: None, + mandate_id: payment_attempt + .mandate_id + .clone() + .map(|id| api_models::payments::MandateIds { + mandate_id: id, + mandate_reference_id: None, }), - mandate_connector: None, - setup_mandate: None, - token: None, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: Some(request.force_sync), - payment_method_data: None, - force_sync: Some( - request.force_sync - && (helpers::check_force_psync_precondition(&payment_attempt.status) - || contains_encoded_data), - ), - payment_attempt, - refunds, - disputes, - attempts, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data, - redirect_response: None, - payment_link_data: None, - surcharge_details: None, - frm_message: frm_response.ok(), + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - None, - )) + confirm: Some(request.force_sync), + payment_method_data: None, + force_sync: Some( + request.force_sync + && (helpers::check_force_psync_precondition(&payment_attempt.status) + || contains_encoded_data), + ), + payment_attempt, + refunds, + disputes, + attempts, + sessions_token: vec![], + card_cvc: None, + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data, + redirect_response: None, + payment_link_data: None, + surcharge_details: None, + frm_message: frm_response.ok(), + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(operation), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } impl diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 53a768f26810..6833a6a392e7 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -44,11 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - PaymentData, - Option, - )> { + ) -> RouterResult> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); let payment_id = payment_id @@ -304,48 +300,67 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); - Ok(( - next_operation, - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id, - mandate_connector, - token, - setup_mandate, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details, - frm_message: None, - payment_link_data: None, + let payment_data = PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: request.email.clone(), + mandate_id, + mandate_connector, + token, + setup_mandate, + address: PaymentAddress { + shipping: shipping_address.as_ref().map(|a| a.into()), + billing: billing_address.as_ref().map(|a| a.into()), }, - Some(customer_details), - )) + confirm: request.confirm, + payment_method_data: request.payment_method_data.clone(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details, + frm_message: None, + payment_link_data: None, + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: next_operation, + customer_details: Some(customer_details), + payment_data, + business_profile, + }; + + Ok(get_trackers_response) } } From 922dc90019deeab4afcd65e6bc1d1c749bdaa09d Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:29:51 +0530 Subject: [PATCH 019/443] ci(hotfix-pr-check): use env input from GitHub to read PR body (#2923) --- .github/workflows/hotfix-pr-check.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml index 59e0bbee3cb4..7a724b602586 100644 --- a/.github/workflows/hotfix-pr-check.yml +++ b/.github/workflows/hotfix-pr-check.yml @@ -19,8 +19,9 @@ jobs: - name: Get hotfix pull request body shell: bash - run: | - echo '${{ github.event.pull_request.body }}' > hotfix_pr_body.txt + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: echo $PR_BODY > hotfix_pr_body.txt - name: Get a list of all original PR numbers shell: bash From cfabfa60db4d275066be72ee64153a34d38f13b8 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 20 Nov 2023 20:52:56 +0530 Subject: [PATCH 020/443] fix: api lock on PaymentsCreate (#2916) --- crates/api_models/src/payments.rs | 14 +++++++ .../payments/operations/payment_approve.rs | 20 ++++------ .../operations/payment_complete_authorize.rs | 19 ++++----- .../payments/operations/payment_confirm.rs | 23 +++++------ .../payments/operations/payment_create.rs | 15 ++----- .../payments/operations/payment_update.rs | 19 ++++----- crates/router/src/routes/payments.rs | 39 +++++++++++++++++-- 7 files changed, 87 insertions(+), 62 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 9f4f151c2228..c427088d688d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1679,6 +1679,20 @@ impl std::fmt::Display for PaymentIdType { } } +impl PaymentIdType { + pub fn and_then(self, f: F) -> Result + where + F: FnOnce(String) -> Result, + { + match self { + Self::PaymentIntentId(s) => f(s).map(Self::PaymentIntentId), + Self::ConnectorTransactionId(s) => f(s).map(Self::ConnectorTransactionId), + Self::PaymentAttemptId(s) => f(s).map(Self::PaymentAttemptId), + Self::PreprocessingId(s) => f(s).map(Self::PreprocessingId), + } + } +} + impl Default for PaymentIdType { fn default() -> Self { Self::PaymentIntentId(Default::default()) diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 78eb3fb1f10d..af52105c85d5 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use data_models::mandates::MandateData; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -399,15 +399,6 @@ impl ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; - let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) .change_context(errors::ApiErrorResponse::InvalidDataFormat { @@ -419,13 +410,18 @@ impl ValidateRequest ValidateRequest, operations::ValidateResult<'a>, )> { - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -394,13 +390,14 @@ impl ValidateRequest ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -840,14 +832,19 @@ impl ValidateRequest ValidateRequest Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request.payment_id.clone().ok_or(error_stack::report!( + errors::ApiErrorResponse::PaymentNotFound + ))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -555,8 +550,6 @@ impl ValidateRequest ValidateRequest ValidateRequest, )> { helpers::validate_customer_details_in_request(request)?; - let given_payment_id = match &request.payment_id { - Some(id_type) => Some( - id_type - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::PaymentNotFound)?, - ), - None => None, - }; + let payment_id = request + .payment_id + .clone() + .ok_or(report!(errors::ApiErrorResponse::PaymentNotFound))?; let request_merchant_id = request.merchant_id.as_deref(); helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) @@ -635,13 +631,14 @@ impl ValidateRequest, ) -> impl Responder { let flow = Flow::PaymentsCreate; - let payload = json_payload.into_inner(); + let mut payload = json_payload.into_inner(); if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method { return http_not_implemented(); }; + if let Err(err) = get_or_generate_payment_id(&mut payload) { + return api::log_and_return_error_response(err); + } + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -959,6 +967,29 @@ where } } +pub fn get_or_generate_payment_id( + payload: &mut payment_types::PaymentsRequest, +) -> errors::RouterResult<()> { + let given_payment_id = payload + .payment_id + .clone() + .map(|payment_id| { + payment_id + .get_payment_intent_id() + .map_err(|err| err.change_context(errors::ApiErrorResponse::PaymentNotFound)) + }) + .transpose()?; + + let payment_id = + core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay").into_report()?; + + payload.payment_id = Some(api_models::payments::PaymentIdType::PaymentIntentId( + payment_id, + )); + + Ok(()) +} + impl GetLockingInput for payment_types::PaymentsRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction where From 5c4e7c9031f62d63af35da2dcab79eac948e7dbb Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:04:22 +0530 Subject: [PATCH 021/443] refactor: add mapping for ConnectorError in payouts flow (#2608) Co-authored-by: Kashif Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif --- crates/api_models/src/payouts.rs | 2 +- crates/diesel_models/src/payout_attempt.rs | 4 +- crates/diesel_models/src/schema.rs | 2 +- .../src/connector/adyen/transformers.rs | 8 +- crates/router/src/core/errors/utils.rs | 5 + crates/router/src/core/payouts.rs | 31 +- crates/router/src/core/payouts/validator.rs | 18 +- crates/router/src/core/utils.rs | 26 +- .../down.sql | 4 + .../up.sql | 6 + openapi/openapi_spec.json | 6 +- .../collection-dir/adyen_uk/.variable.json | 15 + .../Flow Testcases/Happy Cases/.meta.json | 21 +- .../Flow Testcases/QuickStart/.meta.json | 5 +- .../Merchant Account - Create/request.json | 4 + .../Payment Connector - Create/event.test.js | 13 + .../.event.meta.json | 3 + .../event.prerequest.js | 0 .../Payout Connector - Create/event.test.js | 52 +++ .../Payout Connector - Create/request.json | 293 +++++++++++++++ .../Payout Connector - Create/response.json | 1 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../QuickStart/Payouts - Create/event.test.js | 60 ++++ .../QuickStart/Payouts - Create/request.json | 72 ++++ .../QuickStart/Payouts - Create/response.json | 1 + .../Payouts - Retrieve/.event.meta.json | 3 + .../Payouts - Retrieve/event.test.js | 48 +++ .../Payouts - Retrieve/request.json | 22 ++ .../Payouts - Retrieve/response.json | 1 + .../Flow Testcases/Variation Cases/.meta.json | 3 +- .../.meta.json | 3 + .../ACH Payouts - Create/.event.meta.json | 3 + .../ACH Payouts - Create/event.prerequest.js | 0 .../ACH Payouts - Create/event.test.js | 48 +++ .../ACH Payouts - Create/request.json | 99 ++++++ .../ACH Payouts - Create/response.json | 1 + .../Bacs Payouts - Create/.event.meta.json | 3 + .../Bacs Payouts - Create/event.prerequest.js | 0 .../Bacs Payouts - Create/event.test.js | 48 +++ .../Bacs Payouts - Create/request.json | 74 ++++ .../Bacs Payouts - Create/response.json | 1 + .../adyen_uk/event.prerequest.js | 35 ++ postman/collection-dir/wise/.auth.json | 22 ++ postman/collection-dir/wise/.event.meta.json | 3 + postman/collection-dir/wise/.info.json | 8 + postman/collection-dir/wise/.meta.json | 3 + postman/collection-dir/wise/.variable.json | 100 ++++++ .../wise/Flow Testcases/.meta.json | 3 + .../Flow Testcases/Happy Cases/.meta.json | 6 + .../.meta.json | 3 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../Payouts - Create/event.test.js | 48 +++ .../Payouts - Create/request.json | 72 ++++ .../Payouts - Create/response.json | 1 + .../.meta.json | 3 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../Payouts - Create/event.test.js | 48 +++ .../Payouts - Create/request.json | 72 ++++ .../Payouts - Create/response.json | 1 + .../wise/Flow Testcases/QuickStart/.meta.json | 8 + .../API Key - Create/.event.meta.json | 3 + .../QuickStart/API Key - Create/event.test.js | 46 +++ .../QuickStart/API Key - Create/request.json | 47 +++ .../QuickStart/API Key - Create/response.json | 1 + .../.event.meta.json | 3 + .../event.prerequest.js | 0 .../Merchant Account - Create/event.test.js | 56 +++ .../Merchant Account - Create/request.json | 91 +++++ .../Merchant Account - Create/response.json | 1 + .../.event.meta.json | 3 + .../event.prerequest.js | 0 .../Payout Connector - Create/event.test.js | 39 ++ .../Payout Connector - Create/request.json | 333 ++++++++++++++++++ .../Payout Connector - Create/response.json | 1 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../QuickStart/Payouts - Create/event.test.js | 48 +++ .../QuickStart/Payouts - Create/request.json | 72 ++++ .../QuickStart/Payouts - Create/response.json | 1 + .../Flow Testcases/Variation Cases/.meta.json | 3 + .../.meta.json | 3 + .../Payouts - Create/.event.meta.json | 3 + .../Payouts - Create/event.prerequest.js | 0 .../Payouts - Create/event.test.js | 48 +++ .../Payouts - Create/request.json | 72 ++++ .../Payouts - Create/response.json | 1 + .../wise/Health check/.meta.json | 3 + .../wise/Health check/Health/.event.meta.json | 3 + .../wise/Health check/Health/event.test.js | 4 + .../wise/Health check/Health/request.json | 16 + .../wise/Health check/Health/response.json | 1 + .../collection-dir/wise/event.prerequest.js | 0 postman/collection-dir/wise/event.test.js | 13 + 96 files changed, 2310 insertions(+), 62 deletions(-) create mode 100644 migrations/2023-10-27-064512_alter_payout_profile_id/down.sql create mode 100644 migrations/2023-10-27-064512_alter_payout_profile_id/up.sql create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/.auth.json create mode 100644 postman/collection-dir/wise/.event.meta.json create mode 100644 postman/collection-dir/wise/.info.json create mode 100644 postman/collection-dir/wise/.meta.json create mode 100644 postman/collection-dir/wise/.variable.json create mode 100644 postman/collection-dir/wise/Flow Testcases/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json create mode 100644 postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json create mode 100644 postman/collection-dir/wise/Health check/.meta.json create mode 100644 postman/collection-dir/wise/Health check/Health/.event.meta.json create mode 100644 postman/collection-dir/wise/Health check/Health/event.test.js create mode 100644 postman/collection-dir/wise/Health check/Health/request.json create mode 100644 postman/collection-dir/wise/Health check/Health/response.json create mode 100644 postman/collection-dir/wise/event.prerequest.js create mode 100644 postman/collection-dir/wise/event.test.js diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 5cc5e5118166..f7dba2446e91 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -382,7 +382,7 @@ pub struct PayoutCreateResponse { pub error_code: Option, /// The business profile that is associated with this payment - pub profile_id: Option, + pub profile_id: String, } #[derive(Default, Debug, Clone, Deserialize, ToSchema)] diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index d87ed5319a91..7a2c83061877 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -26,7 +26,7 @@ pub struct PayoutAttempt { pub created_at: PrimitiveDateTime, #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, - pub profile_id: Option, + pub profile_id: String, pub merchant_connector_id: Option, } @@ -51,7 +51,7 @@ impl Default for PayoutAttempt { business_label: None, created_at: now, last_modified_at: now, - profile_id: None, + profile_id: String::default(), merchant_connector_id: None, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index ce974e409a2c..2ce4f2b6d9d4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -755,7 +755,7 @@ diesel::table! { created_at -> Timestamp, last_modified_at -> Timestamp, #[max_length = 64] - profile_id -> Nullable, + profile_id -> Varchar, #[max_length = 32] merchant_connector_id -> Nullable, } diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index ec21c9baa5e9..0243dc085f83 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -4010,8 +4010,12 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC iban: Some(b.iban), tax_id: None, }, - _ => Err(errors::ConnectorError::NotSupported { - message: "Bank transfers via ACH or Bacs are not supported".to_string(), + payouts::BankPayout::Ach(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via ACH is not supported".to_string(), + connector: "Adyen", + })?, + payouts::BankPayout::Bacs(..) => Err(errors::ConnectorError::NotSupported { + message: "Bank transfer via Bacs is not supported".to_string(), connector: "Adyen", })?, }; diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index c3cdf95b87bd..869a5b6bde95 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -400,6 +400,11 @@ impl ConnectorErrorExt for error_stack::Result field_names: field_names.to_vec(), } } + errors::ConnectorError::NotSupported { message, connector } => { + errors::ApiErrorResponse::NotSupported { + message: format!("{} by {}", message, connector), + } + } _ => errors::ApiErrorResponse::InternalServerError, }; err.change_context(error) diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index f1136a35a65a..debc9d124448 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -35,6 +35,7 @@ pub struct PayoutData { pub payout_attempt: storage::PayoutAttempt, pub payout_method_data: Option, pub merchant_connector_account: Option, + pub profile_id: String, } // ********************************************** CORE FLOWS ********************************************** @@ -96,9 +97,7 @@ pub async fn payouts_create_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, -) -> RouterResponse -where -{ +) -> RouterResponse { // Form connector data let connector_data = get_connector_data( &state, @@ -111,7 +110,7 @@ where .await?; // Validate create request - let (payout_id, payout_method_data) = + let (payout_id, payout_method_data, profile_id) = validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries @@ -121,6 +120,7 @@ where &key_store, &req, &payout_id, + &profile_id, &connector_data.connector_name, payout_method_data.as_ref(), ) @@ -561,18 +561,8 @@ pub async fn create_recipient( let customer_details = payout_data.customer_details.to_owned(); let connector_name = connector_data.connector_name.to_string(); - let profile_id = core_utils::get_profile_id_from_business_details( - payout_data.payout_attempt.business_country, - payout_data.payout_attempt.business_label.as_ref(), - merchant_account, - payout_data.payout_attempt.profile_id.as_ref(), - &*state.store, - false, - ) - .await?; - // Create the connector label using {profile_id}_{connector_name} - let connector_label = format!("{profile_id}_{}", connector_name); + let connector_label = format!("{}_{}", payout_data.profile_id, connector_name); let (should_call_connector, _connector_customer_id) = helpers::should_call_payout_connector_create_customer( @@ -1124,6 +1114,7 @@ pub async fn response_handler( } // DB entries +#[allow(clippy::too_many_arguments)] #[cfg(feature = "payouts")] pub async fn payout_create_db_entries( state: &AppState, @@ -1131,6 +1122,7 @@ pub async fn payout_create_db_entries( key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, payout_id: &String, + profile_id: &String, connector_name: &api_enums::PayoutConnectors, stored_payout_method_data: Option<&payouts::PayoutMethodData>, ) -> RouterResult { @@ -1231,8 +1223,7 @@ pub async fn payout_create_db_entries( } else { storage_enums::PayoutStatus::RequiresPayoutMethodData }; - let _id = core_utils::get_or_generate_uuid("payout_attempt_id", None)?; - let payout_attempt_id = format!("{}_{}", merchant_id.to_owned(), payout_id.to_owned()); + let payout_attempt_id = utils::get_payment_attempt_id(payout_id, 1); let payout_attempt_req = storage::PayoutAttemptNew::default() .set_payout_attempt_id(payout_attempt_id.to_string()) @@ -1247,7 +1238,7 @@ pub async fn payout_create_db_entries( .set_payout_token(req.payout_token.to_owned()) .set_created_at(Some(common_utils::date_time::now())) .set_last_modified_at(Some(common_utils::date_time::now())) - .set_profile_id(req.profile_id.to_owned()) + .set_profile_id(Some(profile_id.to_string())) .to_owned(); let payout_attempt = db .insert_payout_attempt(payout_attempt_req) @@ -1269,6 +1260,7 @@ pub async fn payout_create_db_entries( .cloned() .or(stored_payout_method_data.cloned()), merchant_connector_account: None, + profile_id: profile_id.to_owned(), }) } @@ -1318,6 +1310,8 @@ pub async fn make_payout_data( .await .map_or(None, |c| c); + let profile_id = payout_attempt.profile_id.clone(); + Ok(PayoutData { billing_address, customer_details, @@ -1325,5 +1319,6 @@ pub async fn make_payout_data( payout_attempt, payout_method_data: None, merchant_connector_account: None, + profile_id, }) } diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 3793ee523dc3..90e3bca9de1d 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -8,7 +8,6 @@ use crate::{ utils as core_utils, }, db::StorageInterface, - logger, routes::AppState, types::{api::payouts, domain, storage}, utils, @@ -24,8 +23,6 @@ pub async fn validate_uniqueness_of_payout_id_against_merchant_id( let payout = db .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) .await; - - logger::debug!(?payout); match payout { Err(err) => { if err.current_context().is_db_not_found() { @@ -58,7 +55,7 @@ pub async fn validate_create_request( merchant_account: &domain::MerchantAccount, req: &payouts::PayoutCreateRequest, merchant_key_store: &domain::MerchantKeyStore, -) -> RouterResult<(String, Option)> { +) -> RouterResult<(String, Option, String)> { let merchant_id = &merchant_account.merchant_id; // Merchant ID @@ -111,5 +108,16 @@ pub async fn validate_create_request( None => None, }; - Ok((payout_id, payout_method_data)) + // Profile ID + let profile_id = core_utils::get_profile_id_from_business_details( + req.business_country, + req.business_label.as_ref(), + merchant_account, + req.profile_id.as_ref(), + &*state.store, + false, + ) + .await?; + + Ok((payout_id, payout_method_data, profile_id)) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index fb3dc3e7d281..5ffc85fe6709 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -48,33 +48,21 @@ pub async fn get_mca_for_payout<'a>( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payout_data: &PayoutData, -) -> RouterResult<(helpers::MerchantConnectorAccountType, String)> { - let payout_attempt = &payout_data.payout_attempt; - let profile_id = get_profile_id_from_business_details( - payout_attempt.business_country, - payout_attempt.business_label.as_ref(), - merchant_account, - payout_attempt.profile_id.as_ref(), - &*state.store, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payout_attempt")?; +) -> RouterResult { match payout_data.merchant_connector_account.to_owned() { - Some(mca) => Ok((mca, profile_id)), + Some(mca) => Ok(mca), None => { let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.merchant_id.as_str(), None, key_store, - &profile_id, + &payout_data.profile_id, connector_id, - payout_attempt.merchant_connector_id.as_ref(), + payout_data.payout_attempt.merchant_connector_id.as_ref(), ) .await?; - Ok((merchant_connector_account, profile_id)) + Ok(merchant_connector_account) } } } @@ -89,7 +77,7 @@ pub async fn construct_payout_router_data<'a, F>( _request: &api_models::payouts::PayoutRequest, payout_data: &mut PayoutData, ) -> RouterResult> { - let (merchant_connector_account, profile_id) = get_mca_for_payout( + let merchant_connector_account = get_mca_for_payout( state, connector_id, merchant_account, @@ -135,7 +123,7 @@ pub async fn construct_payout_router_data<'a, F>( let payouts = &payout_data.payouts; let payout_attempt = &payout_data.payout_attempt; let customer_details = &payout_data.customer_details; - let connector_label = format!("{profile_id}_{}", payout_attempt.connector); + let connector_label = format!("{}_{}", payout_data.profile_id, payout_attempt.connector); let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) diff --git a/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql b/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql new file mode 100644 index 000000000000..a9e789429ec7 --- /dev/null +++ b/migrations/2023-10-27-064512_alter_payout_profile_id/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE + payout_attempt +ALTER COLUMN + profile_id DROP NOT NULL; \ No newline at end of file diff --git a/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql b/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql new file mode 100644 index 000000000000..33355bb9d29c --- /dev/null +++ b/migrations/2023-10-27-064512_alter_payout_profile_id/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE + payout_attempt +ALTER COLUMN + profile_id +SET + NOT NULL; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 65280c187142..9ca4dea4a1a8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10570,7 +10570,8 @@ "entity_type", "status", "error_message", - "error_code" + "error_code", + "profile_id" ], "properties": { "payout_id": { @@ -10702,8 +10703,7 @@ }, "profile_id": { "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true + "description": "The business profile that is associated with this payment" } } }, diff --git a/postman/collection-dir/adyen_uk/.variable.json b/postman/collection-dir/adyen_uk/.variable.json index 514fd88dee71..57b4c958c53f 100644 --- a/postman/collection-dir/adyen_uk/.variable.json +++ b/postman/collection-dir/adyen_uk/.variable.json @@ -39,6 +39,11 @@ "key": "refund_id", "value": "" }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, { "key": "merchant_connector_id", "value": "" @@ -90,6 +95,16 @@ "key": "connector_api_secret", "value": "", "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" } ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json index d99a886e8edb..773ed0638cbf 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json @@ -8,14 +8,17 @@ "Scenario6-Create 3DS payment", "Scenario7-Create 3DS payment with confrm false", "Scenario9-Refund full payment", - "Scenario10-Partial refund", - "Scenario11-Create a mandate and recurring payment", - "Scenario11-Refund recurring payment", - "Scenario16-Bank Redirect-sofort", - "Scenario17-Bank Redirect-eps", - "Scenario18-Bank Redirect-giropay", - "Scenario19-Bank Redirect-Trustly", - "Scenario19-Bank debit-ach", - "Scenario19-Bank debit-Bacs" + "Scenario10-Create a mandate and recurring payment", + "Scenario11-Partial refund", + "Scenario12-Bank Redirect-sofort", + "Scenario13-Bank Redirect-eps", + "Scenario14-Refund recurring payment", + "Scenario15-Bank Redirect-giropay", + "Scenario16-Bank debit-ach", + "Scenario17-Bank debit-Bacs", + "Scenario18-Bank Redirect-Trustly", + "Scenario19-Add card flow", + "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json index c4939d7ab913..45785cf7a484 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/.meta.json @@ -3,9 +3,12 @@ "Merchant Account - Create", "API Key - Create", "Payment Connector - Create", + "Payout Connector - Create", "Payments - Create", "Payments - Retrieve", "Refunds - Create", - "Refunds - Retrieve" + "Refunds - Retrieve", + "Payouts - Create", + "Payouts - Retrieve" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json index dcbf46ee5382..5603ff553ba0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -45,6 +45,10 @@ { "country": "US", "business": "default" + }, + { + "country": "GB", + "business": "payouts" } ], "merchant_details": { diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js index 88e92d8d84a2..96b088be1361 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js @@ -37,3 +37,16 @@ if (jsonData?.merchant_connector_id) { "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", ); } + +// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id +if (jsonData?.profile_id) { + pm.collectionVariables.set("payment_profile_id", jsonData.profile_id); + console.log( + "- use {{payment_profile_id}} as collection variable for value", + jsonData.profile_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js new file mode 100644 index 000000000000..7d0996a0732e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js @@ -0,0 +1,52 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id +if (jsonData?.profile_id) { + pm.collectionVariables.set("payout_profile_id", jsonData.profile_id); + console.log( + "- use {{payout_profile_id}} as collection variable for value", + jsonData.profile_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json new file mode 100644 index 000000000000..0ba1b1689c38 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -0,0 +1,293 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "payout_processor", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "{{connector_api_key}}", + "key1": "{{connector_key1}}", + "api_secret": "{{connector_api_secret}}" + }, + "test_mode": false, + "disabled": false, + "business_country": "GB", + "business_label": "payouts", + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "pay_bright", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "walley", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "paypal", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mobile_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "ali_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "we_chat_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mb_way", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_redirect", + "payment_method_types": [ + { + "payment_method_type": "giropay", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "eps", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "sofort", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "blik", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "trustly", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_czech_republic", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_finland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_poland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_slovakia", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bancontact_card", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_debit", + "payment_method_types": [ + { + "payment_method_type": "ach", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bacs", + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ] + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payout Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js new file mode 100644 index 000000000000..f641cf040d46 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/event.test.js @@ -0,0 +1,60 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payouts/create - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Validate if status is successful +// if (jsonData?.status) { +// pm.test("[POST]::/payouts/create - Content check if value for 'status' matches 'success'", +// function () { +// pm.expect(jsonData.status).to.eql("success"); +// }, +// ); +// } + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json new file mode 100644 index 000000000000..d8ad685ec764 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "EUR", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["adyen"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js new file mode 100644 index 000000000000..e822780ee1e2 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[GET]::/payouts/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payouts/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// Validate if response has JSON Body +pm.test("[GET]::/payouts/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json new file mode 100644 index 000000000000..b7deba38ab27 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/request.json @@ -0,0 +1,22 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": ["{{baseUrl}}"], + "path": ["payouts", ":id"], + "variable": [ + { + "key": "id", + "value": "{{payout_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payouts - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json index fe295640093e..9cbb319a2ae0 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/.meta.json @@ -8,6 +8,7 @@ "Scenario6-Create 3DS payment with greater capture", "Scenario7-Refund exceeds amount", "Scenario8-Refund for unsuccessful payment", - "Scenario9-Create a recurring payment with greater mandate amount" + "Scenario9-Create a recurring payment with greater mandate amount", + "Scenario10-Create payouts using unsupported methods" ] } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json new file mode 100644 index 000000000000..b40f94c032ff --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["ACH Payouts - Create", "Bacs Payouts - Create"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json new file mode 100644 index 000000000000..a2b65418ab75 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/request.json @@ -0,0 +1,99 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 10000, + "currency": "USD", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "Doest John", + "phone": "6168205366", + "phone_country_code": "+1", + "description": "Its my first payout request", + "connector": ["adyen"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_routing_number": "110000000", + "bank_account_number": "000123456789", + "bank_name": "Stripe Test Bank", + "bank_country_code": "US", + "bank_city": "California" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "Doest", + "last_name": "John" + }, + "phone": { + "number": "6168205366", + "country_code": "1" + } + }, + "entity_type": "Individual", + "recurring": false, + "metadata": { + "ref": "123", + "vendor_details": { + "account_type": "custom", + "business_type": "individual", + "business_profile_mcc": 5045, + "business_profile_url": "https://www.pastebin.com", + "business_profile_name": "pT", + "company_address_line1": "address_full_match", + "company_address_line2": "Kimberly Way", + "company_address_postal_code": "31062", + "company_address_city": "Milledgeville", + "company_address_state": "GA", + "company_phone": "+16168205366", + "company_tax_id": "000000000", + "company_owners_provided": false, + "capabilities_card_payments": true, + "capabilities_transfers": true + }, + "individual_details": { + "tos_acceptance_date": 1680581051, + "tos_acceptance_ip": "103.159.11.202", + "individual_dob_day": "01", + "individual_dob_month": "01", + "individual_dob_year": "1901", + "individual_id_number": "000000000", + "individual_ssn_last_4": "0000", + "external_account_account_holder_type": "individual" + } + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/ACH Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json new file mode 100644 index 000000000000..ea00d9e048f8 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/request.json @@ -0,0 +1,74 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "GBP", + "customer_id": "payout_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_sort_code": "231470", + "bank_account_number": "28821822", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true, + "connector": ["adyen"], + "business_label": "abcd", + "business_country": "US" + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create payouts using unsupported methods/Bacs Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/event.prerequest.js b/postman/collection-dir/adyen_uk/event.prerequest.js index e69de29bb2d1..98e1d0e5a27f 100644 --- a/postman/collection-dir/adyen_uk/event.prerequest.js +++ b/postman/collection-dir/adyen_uk/event.prerequest.js @@ -0,0 +1,35 @@ +// Add appropriate profile_id for relevant requests +const path = pm.request.url.toString(); +const isPostRequest = pm.request.method.toString() === "POST"; +const isPaymentCreation = path.match(/\/payments$/) && isPostRequest; +const isPayoutCreation = path.match(/\/payouts\/create$/) && isPostRequest; + +if (isPaymentCreation || isPayoutCreation) { + try { + const request = JSON.parse(pm.request.body.toJSON().raw); + + // Attach profile_id + const profile_id = isPaymentCreation + ? pm.collectionVariables.get("payment_profile_id") + : pm.collectionVariables.get("payout_profile_id"); + request["profile_id"] = profile_id; + + // Attach routing + const routing = { type: "single", data: "adyen" }; + request["routing"] = routing; + + let updatedRequest = { + mode: "raw", + raw: JSON.stringify(request), + options: { + raw: { + language: "json", + }, + }, + }; + pm.request.body.update(updatedRequest); + } catch (error) { + console.error("Failed to inject profile_id in the request"); + console.error(error); + } +} diff --git a/postman/collection-dir/wise/.auth.json b/postman/collection-dir/wise/.auth.json new file mode 100644 index 000000000000..915a28357900 --- /dev/null +++ b/postman/collection-dir/wise/.auth.json @@ -0,0 +1,22 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + } +} diff --git a/postman/collection-dir/wise/.event.meta.json b/postman/collection-dir/wise/.event.meta.json new file mode 100644 index 000000000000..eb871bbcb9bb --- /dev/null +++ b/postman/collection-dir/wise/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.prerequest.js", "event.test.js"] +} diff --git a/postman/collection-dir/wise/.info.json b/postman/collection-dir/wise/.info.json new file mode 100644 index 000000000000..188afe443517 --- /dev/null +++ b/postman/collection-dir/wise/.info.json @@ -0,0 +1,8 @@ +{ + "info": { + "_postman_id": "b5107328-6e3c-4ef0-b575-4072bc64462a", + "name": "wise", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + } +} diff --git a/postman/collection-dir/wise/.meta.json b/postman/collection-dir/wise/.meta.json new file mode 100644 index 000000000000..d513035ce2d6 --- /dev/null +++ b/postman/collection-dir/wise/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Health check", "Flow Testcases"] +} diff --git a/postman/collection-dir/wise/.variable.json b/postman/collection-dir/wise/.variable.json new file mode 100644 index 000000000000..7ac96230fcb0 --- /dev/null +++ b/postman/collection-dir/wise/.variable.json @@ -0,0 +1,100 @@ +{ + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/.meta.json b/postman/collection-dir/wise/Flow Testcases/.meta.json new file mode 100644 index 000000000000..023989e1e494 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["QuickStart", "Happy Cases", "Variation Cases"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json new file mode 100644 index 000000000000..67c98ebd314a --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Scenario1 - Process Bacs Payout", + "Scenario2 - Process SEPA Payout" + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json new file mode 100644 index 000000000000..9189968ecf7d --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "GBP", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_sort_code": "231470", + "bank_account_number": "28821822", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true, + "connector": ["wise"] + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario1 - Process Bacs Payout/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json new file mode 100644 index 000000000000..fbaf31c36a37 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "EUR", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Happy Cases/Scenario2 - Process SEPA Payout/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json new file mode 100644 index 000000000000..935df6d4e112 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Merchant Account - Create", + "API Key - Create", + "Payout Connector - Create", + "Payouts - Create" + ] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js new file mode 100644 index 000000000000..4e27c5a50253 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/event.test.js @@ -0,0 +1,46 @@ +// Validate status 2xx +pm.test("[POST]::/api_keys/:merchant_id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/api_keys/:merchant_id - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id +if (jsonData?.key_id) { + pm.collectionVariables.set("api_key_id", jsonData.key_id); + console.log( + "- use {{api_key_id}} as collection variable for value", + jsonData.key_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json new file mode 100644 index 000000000000..6ceefe5d24cd --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/request.json @@ -0,0 +1,47 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw_json_formatted": { + "name": "API Key 1", + "description": null, + "expiration": "2069-09-23T01:02:03.000Z" + } + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": ["{{baseUrl}}"], + "path": ["api_keys", ":merchant_id"], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/API Key - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js new file mode 100644 index 000000000000..7de0d5beb316 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js @@ -0,0 +1,56 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/accounts - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id +if (jsonData?.merchant_id) { + pm.collectionVariables.set("merchant_id", jsonData.merchant_id); + console.log( + "- use {{merchant_id}} as collection variable for value", + jsonData.merchant_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.", + ); +} + +// pm.collectionVariables - Set api_key as variable for jsonData.api_key +if (jsonData?.api_key) { + pm.collectionVariables.set("api_key", jsonData.api_key); + console.log( + "- use {{api_key}} as collection variable for value", + jsonData.api_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.", + ); +} + +// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key +if (jsonData?.publishable_key) { + pm.collectionVariables.set("publishable_key", jsonData.publishable_key); + console.log( + "- use {{publishable_key}} as collection variable for value", + jsonData.publishable_key, + ); +} else { + console.log( + "INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json new file mode 100644 index 000000000000..dcbf46ee5382 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -0,0 +1,91 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "merchant_id": "postman_merchant_GHAction_{{$guid}}", + "locker_id": "m0010", + "merchant_name": "NewAge Retailer", + "primary_business_details": [ + { + "country": "US", + "business": "default" + } + ], + "merchant_details": { + "primary_contact_person": "John Test", + "primary_email": "JohnTest@test.com", + "primary_phone": "sunt laborum", + "secondary_contact_person": "John Test2", + "secondary_email": "JohnTest2@test.com", + "secondary_phone": "cillum do dolor id", + "website": "www.example.com", + "about_business": "Online Retail with a wide selection of organic products for North America", + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US" + } + }, + "return_url": "https://duck.com", + "webhook_details": { + "webhook_version": "1.0.1", + "webhook_username": "ekart_retail", + "webhook_password": "password_ekart@123", + "payment_created_enabled": true, + "payment_succeeded_enabled": true, + "payment_failed_enabled": true + }, + "sub_merchants_enabled": false, + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": ["{{baseUrl}}"], + "path": ["accounts"] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Merchant Account - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js new file mode 100644 index 000000000000..88e92d8d84a2 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/event.test.js @@ -0,0 +1,39 @@ +// Validate status 2xx +pm.test( + "[POST]::/account/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/account/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json new file mode 100644 index 000000000000..817114b426a7 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -0,0 +1,333 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "payout_processor", + "connector_name": "wise", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{connector_api_key}}", + "key1": "{{connector_key1}}" + }, + "test_mode": false, + "disabled": false, + "business_country": "US", + "business_label": "default", + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "pay_bright", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "walley", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "wallet", + "payment_method_types": [ + { + "payment_method_type": "paypal", + "payment_experience": "redirect_to_url", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "google_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "apple_pay", + "payment_experience": "invoke_sdk_client", + "card_networks": null, + "accepted_currencies": null, + "accepted_countries": null, + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mobile_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "ali_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "we_chat_pay", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "mb_way", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_redirect", + "payment_method_types": [ + { + "payment_method_type": "giropay", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "eps", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "sofort", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "blik", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "trustly", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_czech_republic", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_finland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_poland", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "online_banking_slovakia", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bancontact_card", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "bank_debit", + "payment_method_types": [ + { + "payment_method_type": "ach", + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "bacs", + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "google_pay": { + "allowed_payment_methods": [ + { + "type": "CARD", + "parameters": { + "allowed_auth_methods": ["PAN_ONLY", "CRYPTOGRAM_3DS"], + "allowed_card_networks": [ + "AMEX", + "DISCOVER", + "INTERAC", + "JCB", + "MASTERCARD", + "VISA" + ] + }, + "tokenization_specification": { + "type": "PAYMENT_GATEWAY" + } + } + ], + "merchant_info": { + "merchant_name": "Narayan Bhat" + } + }, + "apple_pay": { + "session_token_data": { + "initiative": "web", + "certificate": "{{certificate}}", + "display_name": "applepay", + "certificate_keys": "{{certificate_keys}}", + "initiative_context": "hyperswitch-sdk-test.netlify.app", + "merchant_identifier": "merchant.com.stripe.sang" + }, + "payment_request_data": { + "label": "applepay pvt.ltd", + "supported_networks": ["visa", "masterCard", "amex", "discover"], + "merchant_capabilities": ["supports3DS"] + } + } + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js new file mode 100644 index 000000000000..4ddb0243d6c6 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 2xx +pm.test("[POST]::/payouts/create - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json new file mode 100644 index 000000000000..fbaf31c36a37 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1, + "currency": "EUR", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "iban": "NL46TEST0136169112", + "bic": "ABNANL2A", + "bank_name": "Deutsche Bank", + "bank_country_code": "NL", + "bank_city": "Amsterdam" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "John", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + "entity_type": "Individual", + "recurring": true, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json new file mode 100644 index 000000000000..972765b13ea5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Scenario1 - Create ACH payout with invalid data"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json new file mode 100644 index 000000000000..c6b765ca0b04 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payouts - Create"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json new file mode 100644 index 000000000000..220b1a6723d5 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js", "event.prerequest.js"] +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js new file mode 100644 index 000000000000..7cf9090d6c5e --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/event.test.js @@ -0,0 +1,48 @@ +// Validate status 4xx +pm.test("[POST]::/payouts/create - Status code is 4xx", function () { + pm.response.to.be.clientError; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payouts/create - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payouts/create - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id +if (jsonData?.payout_id) { + pm.collectionVariables.set("payout_id", jsonData.payout_id); + console.log( + "- use {{payout_id}} as collection variable for value", + jsonData.payout_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json new file mode 100644 index 000000000000..02e8169b787b --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/request.json @@ -0,0 +1,72 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 10000, + "currency": "USD", + "customer_id": "wise_customer", + "email": "payout_customer@example.com", + "name": "Doest John", + "phone": "6168205366", + "phone_country_code": "+1", + "description": "Its my first payout request", + "connector": ["wise"], + "payout_type": "bank", + "payout_method_data": { + "bank": { + "bank_routing_number": "110000000", + "bank_account_number": "000123456789", + "bank_name": "Stripe Test Bank", + "bank_country_code": "US", + "bank_city": "California" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "CA", + "zip": "94122", + "country": "US", + "first_name": "Doest", + "last_name": "John" + }, + "phone": { + "number": "6168205366", + "country_code": "1" + } + }, + "entity_type": "Individual", + "recurring": false, + "metadata": { + "ref": "123" + }, + "confirm": true, + "auto_fulfill": true + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": ["{{baseUrl}}"], + "path": ["payouts", "create"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Flow Testcases/Variation Cases/Scenario1 - Create ACH payout with invalid data/Payouts - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/Health check/.meta.json b/postman/collection-dir/wise/Health check/.meta.json new file mode 100644 index 000000000000..f5da236cd01f --- /dev/null +++ b/postman/collection-dir/wise/Health check/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Health"] +} diff --git a/postman/collection-dir/wise/Health check/Health/.event.meta.json b/postman/collection-dir/wise/Health check/Health/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/wise/Health check/Health/event.test.js b/postman/collection-dir/wise/Health check/Health/event.test.js new file mode 100644 index 000000000000..b490b8be090f --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/event.test.js @@ -0,0 +1,4 @@ +// Validate status 2xx +pm.test("[POST]::/accounts - Status code is 2xx", function () { + pm.response.to.be.success; +}); diff --git a/postman/collection-dir/wise/Health check/Health/request.json b/postman/collection-dir/wise/Health check/Health/request.json new file mode 100644 index 000000000000..e40e93961785 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/request.json @@ -0,0 +1,16 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": ["{{baseUrl}}"], + "path": ["health"] + } +} diff --git a/postman/collection-dir/wise/Health check/Health/response.json b/postman/collection-dir/wise/Health check/Health/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/wise/Health check/Health/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/wise/event.prerequest.js b/postman/collection-dir/wise/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/wise/event.test.js b/postman/collection-dir/wise/event.test.js new file mode 100644 index 000000000000..fb52caec30fc --- /dev/null +++ b/postman/collection-dir/wise/event.test.js @@ -0,0 +1,13 @@ +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log("[LOG]::payment_id - " + jsonData.payment_id); +} + +console.log("[LOG]::x-request-id - " + pm.response.headers.get("x-request-id")); From be4aa3b913819698c6c22ddedafe1d90fbe02add Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:53:19 +0530 Subject: [PATCH 022/443] refactor(payment_methods): Added support for pm_auth_connector field in pm list response (#2667) Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Co-authored-by: Shanks --- crates/api_models/src/payment_methods.rs | 3 +++ crates/router/src/core/payment_methods/cards.rs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 755acbf7f425..c40dffe4cf31 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -325,6 +325,9 @@ pub struct ResponsePaymentMethodTypes { } "#)] pub surcharge_details: Option, + + /// auth service connector label for this payment method type, if exists + pub pm_auth_connector: Option, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f2eeedf5388f..2fe3a75d80ee 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1384,6 +1384,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, }) } @@ -1418,6 +1419,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, }) } @@ -1447,6 +1449,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1479,6 +1482,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } @@ -1511,6 +1515,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, + pm_auth_connector: None, } }) } From e566a4eff2270c2a56ec90966f42ccfd79906068 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 21 Nov 2023 15:41:35 +0530 Subject: [PATCH 023/443] fix: merchant_connector_id null in KV flow (#2810) Co-authored-by: preetamrevankar <132073736+preetamrevankar@users.noreply.github.com> --- crates/diesel_models/src/payment_attempt.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index bb8f2b60bbb7..f77e75491d86 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -359,7 +359,9 @@ impl PaymentAttemptUpdate { .amount_capturable .unwrap_or(source.amount_capturable), updated_by: pa_update.updated_by, - merchant_connector_id: pa_update.merchant_connector_id, + merchant_connector_id: pa_update + .merchant_connector_id + .or(source.merchant_connector_id), authentication_data: pa_update.authentication_data.or(source.authentication_data), encoded_data: pa_update.encoded_data.or(source.encoded_data), unified_code: pa_update.unified_code.unwrap_or(source.unified_code), From 938b63a1fceb87b4aae4211dac4d051e024028b1 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:41:07 +0530 Subject: [PATCH 024/443] fix(connector): [CASHTOCODE] Fix Error Response Handling (#2926) --- crates/router/src/connector/cashtocode/transformers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 2caef69db92c..42e47c077e8c 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -289,7 +289,7 @@ impl #[derive(Debug, Deserialize)] pub struct CashtocodeErrorResponse { - pub error: String, + pub error: serde_json::Value, pub error_description: String, pub errors: Option>, } From d8fcd3c9712480c1230590c4f23b35da79df784d Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 21 Nov 2023 19:44:40 +0530 Subject: [PATCH 025/443] refactor(connector): [Paypal] Add support for both BodyKey and SignatureKey (#2633) Co-authored-by: Mani Chandra Dulam Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> --- crates/router/src/connector/paypal.rs | 90 ++++-- .../src/connector/paypal/transformers.rs | 291 ++++++++++++++++-- 2 files changed, 325 insertions(+), 56 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index e514ebbed2fc..0e8cff8c0569 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -5,10 +5,10 @@ use base64::Engine; use common_utils::ext_traits::ByteSliceExt; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface, Secret}; use transformers as paypal; -use self::transformers::{PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; +use self::transformers::{auth_headers, PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; use super::utils::PaymentsCompleteAuthorizeRequestData; use crate::{ configs::settings, @@ -31,7 +31,7 @@ use crate::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, transformers::ForeignFrom, - ErrorResponse, Response, + ConnectorAuthType, ErrorResponse, Response, }, utils::{self, BytesExt}, }; @@ -110,8 +110,8 @@ where .clone() .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; let key = &req.attempt_id; - - Ok(vec![ + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let mut headers = vec![ ( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), @@ -121,17 +121,57 @@ where format!("Bearer {}", access_token.token.peek()).into_masked(), ), ( - "Prefer".to_string(), + auth_headers::PREFER.to_string(), "return=representation".to_string().into(), ), ( - "PayPal-Request-Id".to_string(), + auth_headers::PAYPAL_REQUEST_ID.to_string(), key.to_string().into_masked(), ), - ]) + ]; + if let Ok(paypal::PaypalConnectorCredentials::PartnerIntegration(credentials)) = + auth.get_credentials() + { + let auth_assertion_header = + construct_auth_assertion_header(&credentials.payer_id, &credentials.client_id); + headers.extend(vec![ + ( + auth_headers::PAYPAL_AUTH_ASSERTION.to_string(), + auth_assertion_header.to_string().into_masked(), + ), + ( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchPPCP_SP".to_string().into(), + ), + ]) + } else { + headers.extend(vec![( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchlegacy_Ecom".to_string().into(), + )]) + } + Ok(headers) } } +fn construct_auth_assertion_header( + payer_id: &Secret, + client_id: &Secret, +) -> String { + let algorithm = consts::BASE64_ENGINE + .encode("{\"alg\":\"none\"}") + .to_string(); + let merchant_credentials = format!( + "{{\"iss\":\"{}\",\"payer_id\":\"{}\"}}", + client_id.clone().expose(), + payer_id.clone().expose() + ); + let encoded_credentials = consts::BASE64_ENGINE + .encode(merchant_credentials) + .to_string(); + format!("{algorithm}.{encoded_credentials}.") +} + impl ConnectorCommon for Paypal { fn id(&self) -> &'static str { "paypal" @@ -151,14 +191,14 @@ impl ConnectorCommon for Paypal { fn get_auth_header( &self, - auth_type: &types::ConnectorAuthType, + auth_type: &ConnectorAuthType, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = auth_type - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth = paypal::PaypalAuthType::try_from(auth_type)?; + let credentials = auth.get_credentials()?; + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.into_masked(), + credentials.get_client_secret().into_masked(), )]) } @@ -260,15 +300,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( @@ -998,15 +1032,9 @@ impl >, _connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 5468c6bb8061..d023077ff008 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1,7 +1,8 @@ use api_models::{enums, payments::BankRedirectData}; +use base64::Engine; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -11,10 +12,11 @@ use crate::{ self, to_connector_meta, AccessTokenRequestInfo, AddressDetailsData, BankRedirectBillingData, CardData, PaymentsAuthorizeRequestData, }, + consts, core::errors, services, types::{ - self, api, storage::enums as storage_enums, transformers::ForeignFrom, + self, api, storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, VerifyWebhookSourceResponseData, }, }; @@ -57,6 +59,12 @@ mod webhook_headers { pub const PAYPAL_CERT_URL: &str = "paypal-cert-url"; pub const PAYPAL_AUTH_ALGO: &str = "paypal-auth-algo"; } +pub mod auth_headers { + pub const PAYPAL_PARTNER_ATTRIBUTION_ID: &str = "PayPal-Partner-Attribution-Id"; + pub const PREFER: &str = "Prefer"; + pub const PAYPAL_REQUEST_ID: &str = "PayPal-Request-Id"; + pub const PAYPAL_AUTH_ASSERTION: &str = "PayPal-Auth-Assertion"; +} #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "UPPERCASE")] @@ -72,19 +80,111 @@ pub struct OrderAmount { pub value: String, } +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct OrderRequestAmount { + pub currency_code: storage_enums::Currency, + pub value: String, + pub breakdown: AmountBreakdown, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for OrderRequestAmount { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + breakdown: AmountBreakdown { + item_total: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + }, + }, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AmountBreakdown { + item_total: OrderAmount, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct PurchaseUnitRequest { reference_id: Option, //reference for an item in purchase_units invoice_id: Option, //The API caller-provided external invoice number for this order. Appears in both the payer's transaction history and the emails that the payer receives. custom_id: Option, //Used to reconcile client transactions with PayPal transactions. - amount: OrderAmount, + amount: OrderRequestAmount, + #[serde(skip_serializing_if = "Option::is_none")] + payee: Option, + shipping: Option, + items: Vec, } -#[derive(Debug, Serialize)] +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct Payee { + merchant_id: Secret, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ItemDetails { + name: String, + quantity: u16, + unit_amount: OrderAmount, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetails { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + name: format!( + "Payment for invoice {}", + item.router_data.connector_request_reference_id + ), + quantity: 1, + unit_amount: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_string(), + }, + } + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Address { address_line_1: Option>, postal_code: Option>, country_code: api_models::enums::CountryAlpha2, + admin_area_2: Option, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingAddress { + address: Option
, + name: Option, +} + +impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ShippingAddress { + type Error = error_stack::Report; + + fn try_from( + item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + Ok(Self { + address: get_address_info(item.router_data.address.shipping.as_ref())?, + name: Some(ShippingName { + full_name: item + .router_data + .address + .shipping + .as_ref() + .and_then(|inner_data| inner_data.address.as_ref()) + .and_then(|inner_data| inner_data.first_name.clone()), + }), + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingName { + full_name: Option>, } #[derive(Debug, Serialize)] @@ -124,6 +224,22 @@ pub struct RedirectRequest { pub struct ContextStruct { return_url: Option, cancel_url: Option, + user_action: Option, + shipping_preference: ShippingPreference, +} + +#[derive(Debug, Serialize)] +pub enum UserAction { + #[serde(rename = "PAY_NOW")] + PayNow, +} + +#[derive(Debug, Serialize)] +pub enum ShippingPreference { + #[serde(rename = "SET_PROVIDED_ADDRESS")] + SetProvidedAddress, + #[serde(rename = "GET_FROM_FILE")] + GetFromFile, } #[derive(Debug, Serialize)] @@ -158,6 +274,7 @@ fn get_address_info( country_code: address.get_country()?.to_owned(), address_line_1: address.line1.clone(), postal_code: address.zip.clone(), + admin_area_2: address.city.clone(), }), None => None, }; @@ -180,6 +297,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Giropay { @@ -194,6 +317,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Ideal { @@ -208,6 +337,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Sofort { @@ -220,6 +355,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::BancontactCard { .. } @@ -247,11 +388,24 @@ fn get_payment_source( } } +fn get_payee(auth_type: &PaypalAuthType) -> Option { + auth_type + .get_credentials() + .ok() + .and_then(|credentials| credentials.get_payer_id()) + .map(|payer_id| Payee { + merchant_id: payer_id, + }) +} + impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalPaymentsRequest { type Error = error_stack::Report; fn try_from( item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { + let paypal_auth: PaypalAuthType = + PaypalAuthType::try_from(&item.router_data.connector_auth_type)?; + let payee = get_payee(&paypal_auth); match item.router_data.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { let intent = if item.router_data.request.is_auto_capture()? { @@ -259,18 +413,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_request_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_request_reference_id.clone()), custom_id: Some(connector_request_reference_id.clone()), invoice_id: Some(connector_request_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let card = item.router_data.request.get_card()?; let expiry = Some(card.get_expiry_date_as_yyyymm("-")); @@ -306,25 +462,29 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { experience_context: ContextStruct { return_url: item.router_data.request.complete_authorize_url.clone(), cancel_url: item.router_data.request.complete_authorize_url.clone(), + shipping_preference: ShippingPreference::SetProvidedAddress, + user_action: Some(UserAction::PayNow), }, })); @@ -374,18 +534,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP connector: "Paypal".to_string(), })? }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(get_payment_source(item.router_data, bank_redirection_data)?); @@ -604,19 +766,98 @@ impl TryFrom, - pub(super) key1: Secret, +pub enum PaypalAuthType { + TemporaryAuth, + AuthWithDetails(PaypalConnectorCredentials), +} + +#[derive(Debug)] +pub enum PaypalConnectorCredentials { + StandardIntegration(StandardFlowCredentials), + PartnerIntegration(PartnerFlowCredentials), } -impl TryFrom<&types::ConnectorAuthType> for PaypalAuthType { +impl PaypalConnectorCredentials { + pub fn get_client_id(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_id.clone(), + Self::PartnerIntegration(item) => item.client_id.clone(), + } + } + + pub fn get_client_secret(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_secret.clone(), + Self::PartnerIntegration(item) => item.client_secret.clone(), + } + } + + pub fn get_payer_id(&self) -> Option> { + match self { + Self::StandardIntegration(_) => None, + Self::PartnerIntegration(item) => Some(item.payer_id.clone()), + } + } + + pub fn generate_authorization_value(&self) -> String { + let auth_id = format!( + "{}:{}", + self.get_client_id().expose(), + self.get_client_secret().expose(), + ); + format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id)) + } +} + +#[derive(Debug)] +pub struct StandardFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, +} + +#[derive(Debug)] +pub struct PartnerFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, + pub(super) payer_id: Secret, +} + +impl PaypalAuthType { + pub fn get_credentials( + &self, + ) -> CustomResult<&PaypalConnectorCredentials, errors::ConnectorError> { + match self { + Self::TemporaryAuth => Err(errors::ConnectorError::InvalidConnectorConfig { + config: "TemporaryAuth found in connector_account_details", + } + .into()), + Self::AuthWithDetails(credentials) => Ok(credentials), + } + } +} + +impl TryFrom<&ConnectorAuthType> for PaypalAuthType { type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { - api_key: api_key.to_owned(), - key1: key1.to_owned(), - }), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::StandardIntegration(StandardFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + }), + )), + types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::PartnerIntegration(PartnerFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + payer_id: api_secret.to_owned(), + }), + )), + types::ConnectorAuthType::TemporaryAuth => Ok(Self::TemporaryAuth), _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, } } From ce725ef8c680eea3fe03671c989fd4572cfc0640 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:59:18 +0000 Subject: [PATCH 026/443] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 6612 +++++++++-------- .../wise.postman_collection.json | 1025 +++ 2 files changed, 4642 insertions(+), 2995 deletions(-) create mode 100644 postman/collection-json/wise.postman_collection.json diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 716b6d9d0699..ad916657948f 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -4,6 +4,41 @@ "listen": "prerequest", "script": { "exec": [ + "// Add appropriate profile_id for relevant requests", + "const path = pm.request.url.toString();", + "const isPostRequest = pm.request.method.toString() === \"POST\";", + "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", + "const isPayoutCreation = path.match(/\\/payouts\\/create$/) && isPostRequest;", + "", + "if (isPaymentCreation || isPayoutCreation) {", + " try {", + " const request = JSON.parse(pm.request.body.toJSON().raw);", + "", + " // Attach profile_id", + " const profile_id = isPaymentCreation", + " ? pm.collectionVariables.get(\"payment_profile_id\")", + " : pm.collectionVariables.get(\"payout_profile_id\");", + " request[\"profile_id\"] = profile_id;", + "", + " // Attach routing", + " const routing = { type: \"single\", data: \"adyen\" };", + " request[\"routing\"] = routing;", + "", + " let updatedRequest = {", + " mode: \"raw\",", + " raw: JSON.stringify(request),", + " options: {", + " raw: {", + " language: \"json\",", + " },", + " },", + " };", + " pm.request.body.update(updatedRequest);", + " } catch (error) {", + " console.error(\"Failed to inject profile_id in the request\");", + " console.error(error);", + " }", + "}", "" ], "type": "text/javascript" @@ -200,7 +235,7 @@ "language": "json" } }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"},{\"country\":\"GB\",\"business\":\"payouts\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/accounts", @@ -370,6 +405,19 @@ " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", " );", "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payment_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payment_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", "" ], "type": "text/javascript" @@ -448,6 +496,143 @@ }, "response": [] }, + { + "name": "Payout Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payout_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payout_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, { "name": "Payments - Create", "event": [ @@ -816,180 +1001,243 @@ "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ + }, { - "name": "Scenario10-Create a mandate and recurring payment", - "item": [ + "name": "Payouts - Create", + "event": [ { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payouts/create - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Validate if status is successful", + "// if (jsonData?.status) {", + "// pm.test(\"[POST]::/payouts/create - Content check if value for 'status' matches 'success'\",", + "// function () {", + "// pm.expect(jsonData.status).to.eql(\"success\");", + "// },", + "// );", + "// }", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payouts - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payouts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payouts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payouts/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payout_id}}", + "description": "(Required) unique payment id" + } + ] }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1038,31 +1286,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1070,61 +1302,60 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Recurring Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1173,39 +1404,15 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1213,60 +1420,66 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ { - "name": "Payments - Retrieve-copy", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1315,31 +1528,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -1347,66 +1544,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario11-Partial refund", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -1458,7 +1652,7 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -1471,6 +1665,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -1489,18 +1703,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -1573,10 +1796,10 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -1622,61 +1845,87 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ { - "name": "Refunds - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -1705,38 +1954,46 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -1745,35 +2002,51 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -1784,88 +2057,143 @@ } ], "request": { - "method": "GET", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" + "payments", + ":id", + "confirm" ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Create-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"1000\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -1876,93 +2204,120 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, - { - "name": "Refunds - Retrieve-copy", + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", @@ -1973,55 +2328,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } + "payments" ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2070,20 +2433,35 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -2091,27 +2469,35 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "capture" ], "variable": [ { @@ -2121,36 +2507,31 @@ } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To capture the funds for an uncaptured payment" }, "response": [] - } - ] - }, - { - "name": "Scenario12-Bank Redirect-sofort", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2199,12 +2580,12 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -2215,63 +2596,60 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2320,41 +2698,120 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", " },", ");", "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", - " },", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -2365,26 +2822,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -2403,17 +2840,17 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id/cancel", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "confirm" + "cancel" ], "variable": [ { @@ -2423,7 +2860,7 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" }, "response": [] }, @@ -2496,12 +2933,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"cancelled\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", " },", " );", "}", @@ -2549,7 +2986,7 @@ ] }, { - "name": "Scenario13-Bank Redirect-eps", + "name": "Scenario6-Create 3DS payment", "item": [ { "name": "Payments - Create", @@ -2620,15 +3057,24 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -2654,7 +3100,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2670,29 +3116,26 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2744,41 +3187,12 @@ "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"eps\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", - " },", - " );", - "}", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -2786,89 +3200,66 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" ], - "variable": [ + "query": [ { - "key": "id", - "value": "{{payment_id}}", + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", "description": "(Required) unique payment id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario7-Create 3DS payment with confrm false", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -2917,12 +3308,12 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", @@ -2933,66 +3324,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario14-Refund recurring payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3041,39 +3429,22 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", "" @@ -3083,6 +3454,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -3101,18 +3492,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -3185,31 +3585,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3250,9 +3634,14 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario9-Refund full payment", + "item": [ { - "name": "Recurring Payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -3329,40 +3718,6 @@ " },", " );", "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3388,7 +3743,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3404,7 +3759,7 @@ "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -3481,22 +3836,6 @@ " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -3539,7 +3878,7 @@ "response": [] }, { - "name": "Refunds - Create Copy", + "name": "Refunds - Create", "event": [ { "listen": "test", @@ -3636,7 +3975,7 @@ "response": [] }, { - "name": "Refunds - Retrieve Copy", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", @@ -3730,7 +4069,7 @@ ] }, { - "name": "Scenario15-Bank Redirect-giropay", + "name": "Scenario10-Create a mandate and recurring payment", "item": [ { "name": "Payments - Create", @@ -3801,15 +4140,41 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -3835,7 +4200,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3851,29 +4216,26 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3922,44 +4284,31 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"mandate_id\"", "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", " },", ");", "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -3967,55 +4316,27 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -4025,31 +4346,31 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4098,15 +4419,39 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4114,66 +4459,60 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario16-Bank debit-ach", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4222,15 +4561,31 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4238,63 +4593,66 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario11-Partial refund", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4343,35 +4701,15 @@ " );", "}", "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", - " },", - " );", - "}", - "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -4379,26 +4717,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -4417,27 +4735,18 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, @@ -4510,10 +4819,10 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", @@ -4559,87 +4868,61 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario17-Bank debit-Bacs", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -4668,46 +4951,38 @@ "language": "json" } }, - "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Confirm", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", @@ -4716,71 +4991,127 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", - "", - "// Response body should have value \"bacs\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", + " pm.expect(jsonData.status).to.eql(\"pending\");", " },", " );", "}", "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.amount).to.eql(1000);", " },", " );", "}", @@ -4791,26 +5122,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -4829,32 +5140,115 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", @@ -4922,15 +5316,20 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", "" ], "type": "text/javascript" @@ -4975,7 +5374,7 @@ ] }, { - "name": "Scenario18-Bank Redirect-Trustly", + "name": "Scenario12-Bank Redirect-sofort", "item": [ { "name": "Payments - Create", @@ -5080,7 +5479,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5186,12 +5585,12 @@ " },", ");", "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", + "// Response body should have value \"sofort\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", " },", " );", "}", @@ -5250,7 +5649,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -5396,7 +5795,7 @@ ] }, { - "name": "Scenario19-Add card flow", + "name": "Scenario13-Bank Redirect-eps", "item": [ { "name": "Payments - Create", @@ -5405,256 +5804,78 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -5679,7 +5900,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5695,7 +5916,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -5766,16 +5987,34 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"eps\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " },", + " );", + "}", "", "// Response body should have value \"adyen\" for \"connector\"", "if (jsonData?.connector) {", @@ -5831,7 +6070,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -5856,7 +6095,7 @@ "response": [] }, { - "name": "Payments - Retrieve Copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -5924,47 +6163,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -6008,69 +6212,116 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario14-Refund recurring payment", + "item": [ { - "name": "Refunds - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -6096,78 +6347,115 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -6183,88 +6471,145 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6289,7 +6634,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6305,32 +6650,100 @@ "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6345,126 +6758,90 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Save card payments - Create", + "name": "Refunds - Create Copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6489,46 +6866,38 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", @@ -6537,172 +6906,99 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.status).to.eql(\"pending\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", - "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", - " },", - " );", - "}" + "" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "refunds", + ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario15-Bank Redirect-giropay", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -6751,47 +7047,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -6802,108 +7063,176 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -6922,48 +7251,109 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6975,129 +7365,119 @@ { "key": "Accept", "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario16-Bank debit-ach", + "item": [ { - "name": "Save card payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -7122,7 +7502,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -7138,7 +7518,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -7209,44 +7589,36 @@ " );", "}", "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"ach\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", - "}" + "}", + "" ], "type": "text/javascript" } @@ -7291,17 +7663,136 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "confirm" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -7311,31 +7802,36 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario17-Bank debit-Bacs", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7384,47 +7880,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -7435,66 +7896,63 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7543,12 +8001,32 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"bacs\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", + " },", + " );", + "}", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -7559,6 +8037,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -7577,18 +8075,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -7661,12 +8168,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"processing\");", " },", " );", "}", @@ -7714,7 +8221,7 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario18-Bank Redirect-Trustly", "item": [ { "name": "Payments - Create", @@ -7785,12 +8292,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -7819,7 +8326,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -7906,12 +8413,41 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -7960,7 +8496,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -8053,12 +8589,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -8106,7 +8642,7 @@ ] }, { - "name": "Scenario3-Create payment without PMD", + "name": "Scenario19-Add card flow", "item": [ { "name": "Payments - Create", @@ -8115,78 +8651,56 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -8211,7 +8725,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8227,300 +8741,229 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8569,12 +9012,23 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", " },", " );", "}", @@ -8585,6 +9039,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -8603,45 +9077,51 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8690,16 +9170,21 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.amount) {", " pm.test(", @@ -8715,7 +9200,17 @@ " pm.test(", " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -8726,35 +9221,27 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "capture" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -8764,88 +9251,72 @@ } ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", "" ], "type": "text/javascript" @@ -8853,114 +9324,93 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To create a refund against an already processed payment" }, "response": [] - } - ] - }, - { - "name": "Scenario5-Void the payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -8971,108 +9421,96 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "item": [ { - "name": "Payments - Cancel", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -9097,109 +9535,48 @@ "language": "json" } }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } @@ -9214,125 +9591,126 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" + "customers", + ":customer_id", + "payment_methods" ], "query": [ { - "key": "force_sync", - "value": "true" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -9357,7 +9735,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -9373,26 +9751,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9441,43 +9822,99 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", - "" + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"24\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " },", + " );", + "}" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "confirm" ], "variable": [ { @@ -9487,36 +9924,31 @@ } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] - } - ] - }, - { - "name": "Scenario7-Create 3DS payment with confrm false", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -9565,12 +9997,47 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -9581,156 +10048,108 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -9749,109 +10168,48 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } @@ -9866,116 +10224,126 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" + "customers", + ":customer_id", + "payment_methods" ], "query": [ { - "key": "force_sync", - "value": "true" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] - } - ] - }, - { - "name": "Scenario9-Refund full payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -10000,7 +10368,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -10016,26 +10384,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10084,120 +10455,70 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"failed\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.error_code).to.eql(\"24\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -10216,78 +10537,143 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", + " pm.expect(jsonData.status).to.eql(\"failed\");", " },", " );", "}", "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "if (jsonData?.amount) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", " function () {", " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -10303,23 +10689,29 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } @@ -13542,6 +13934,221 @@ "response": [] } ] + }, + { + "name": "Scenario10-Create payouts using unsupported methods", + "item": [ + { + "name": "ACH Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\",\"vendor_details\":{\"account_type\":\"custom\",\"business_type\":\"individual\",\"business_profile_mcc\":5045,\"business_profile_url\":\"https://www.pastebin.com\",\"business_profile_name\":\"pT\",\"company_address_line1\":\"address_full_match\",\"company_address_line2\":\"Kimberly Way\",\"company_address_postal_code\":\"31062\",\"company_address_city\":\"Milledgeville\",\"company_address_state\":\"GA\",\"company_phone\":\"+16168205366\",\"company_tax_id\":\"000000000\",\"company_owners_provided\":false,\"capabilities_card_payments\":true,\"capabilities_transfers\":true},\"individual_details\":{\"tos_acceptance_date\":1680581051,\"tos_acceptance_ip\":\"103.159.11.202\",\"individual_dob_day\":\"01\",\"individual_dob_month\":\"01\",\"individual_dob_year\":\"1901\",\"individual_id_number\":\"000000000\",\"individual_ssn_last_4\":\"0000\",\"external_account_account_holder_type\":\"individual\"}},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Bacs Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"adyen\"],\"business_label\":\"abcd\",\"business_country\":\"US\"}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] } ] } @@ -13614,6 +14221,11 @@ "key": "refund_id", "value": "" }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, { "key": "merchant_connector_id", "value": "" @@ -13665,6 +14277,16 @@ "key": "connector_api_secret", "value": "", "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" } ] } diff --git a/postman/collection-json/wise.postman_collection.json b/postman/collection-json/wise.postman_collection.json new file mode 100644 index 000000000000..dc4d9395d3ac --- /dev/null +++ b/postman/collection-json/wise.postman_collection.json @@ -0,0 +1,1025 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "Health", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payout Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"wise\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1 - Process Bacs Payout", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"wise\"]}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2 - Process SEPA Payout", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario1 - Create ACH payout with invalid data", + "item": [ + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"wise_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"wise\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "b5107328-6e3c-4ef0-b575-4072bc64462a", + "name": "wise", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} From 8f610f4cf13256ee6c0ef534a97d735089fd8f33 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:59:19 +0000 Subject: [PATCH 027/443] chore(version): v1.85.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141bfd40ac5d..bbe558180021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.85.0 (2023-11-21) + +### Features + +- **mca:** Add new `auth_type` and a status field for mca ([#2883](https://github.com/juspay/hyperswitch/pull/2883)) ([`25cef38`](https://github.com/juspay/hyperswitch/commit/25cef386b8876b43893f20b93cd68ece6e68412d)) +- **router:** Add unified_code, unified_message in payments response ([#2918](https://github.com/juspay/hyperswitch/pull/2918)) ([`3954001`](https://github.com/juspay/hyperswitch/commit/39540015fde476ad8492a9142c2c1bfda8444a27)) + +### Bug Fixes + +- **connector:** + - [fiserv] fix metadata deserialization in merchant_connector_account ([#2746](https://github.com/juspay/hyperswitch/pull/2746)) ([`644709d`](https://github.com/juspay/hyperswitch/commit/644709d95f6ecaab497cf0cf3788b9e2ed88b855)) + - [CASHTOCODE] Fix Error Response Handling ([#2926](https://github.com/juspay/hyperswitch/pull/2926)) ([`938b63a`](https://github.com/juspay/hyperswitch/commit/938b63a1fceb87b4aae4211dac4d051e024028b1)) +- **router:** Associate parent payment token with `payment_method_id` as hyperswitch token for saved cards ([#2130](https://github.com/juspay/hyperswitch/pull/2130)) ([`efeebc0`](https://github.com/juspay/hyperswitch/commit/efeebc0f2365f0900de3dd3e10a1539621c9933d)) +- Api lock on PaymentsCreate ([#2916](https://github.com/juspay/hyperswitch/pull/2916)) ([`cfabfa6`](https://github.com/juspay/hyperswitch/commit/cfabfa60db4d275066be72ee64153a34d38f13b8)) +- Merchant_connector_id null in KV flow ([#2810](https://github.com/juspay/hyperswitch/pull/2810)) ([`e566a4e`](https://github.com/juspay/hyperswitch/commit/e566a4eff2270c2a56ec90966f42ccfd79906068)) + +### Refactors + +- **connector:** [Paypal] Add support for both BodyKey and SignatureKey ([#2633](https://github.com/juspay/hyperswitch/pull/2633)) ([`d8fcd3c`](https://github.com/juspay/hyperswitch/commit/d8fcd3c9712480c1230590c4f23b35da79df784d)) +- **core:** Query business profile only once ([#2830](https://github.com/juspay/hyperswitch/pull/2830)) ([`44deeb7`](https://github.com/juspay/hyperswitch/commit/44deeb7e7605cb5320b84c0fac1fd551877803a4)) +- **payment_methods:** Added support for pm_auth_connector field in pm list response ([#2667](https://github.com/juspay/hyperswitch/pull/2667)) ([`be4aa3b`](https://github.com/juspay/hyperswitch/commit/be4aa3b913819698c6c22ddedafe1d90fbe02add)) +- Add mapping for ConnectorError in payouts flow ([#2608](https://github.com/juspay/hyperswitch/pull/2608)) ([`5c4e7c9`](https://github.com/juspay/hyperswitch/commit/5c4e7c9031f62d63af35da2dcab79eac948e7dbb)) + +### Testing + +- **postman:** Update postman collection files ([`ce725ef`](https://github.com/juspay/hyperswitch/commit/ce725ef8c680eea3fe03671c989fd4572cfc0640)) + +**Full Changelog:** [`v1.84.0...v1.85.0`](https://github.com/juspay/hyperswitch/compare/v1.84.0...v1.85.0) + +- - - + + ## 1.84.0 (2023-11-17) ### Features From 3f3b797dc65c1bc6f710b122ef00d5bcb409e600 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:25:38 +0530 Subject: [PATCH 028/443] fix: status goes from pending to partially captured in psync (#2915) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../src/payments/payment_attempt.rs | 7 ++++-- crates/diesel_models/src/payment_attempt.rs | 17 ++++++++++---- crates/router/src/connector/utils.rs | 20 ++++++++++++---- crates/router/src/core/payments/helpers.rs | 23 +++++++++++++++++++ .../payments/operations/payment_capture.rs | 23 +++++++++++++------ .../payments/operations/payment_create.rs | 14 ++++------- .../payments/operations/payment_response.rs | 2 ++ crates/router/src/core/payments/retry.rs | 2 ++ crates/router/src/types.rs | 11 +++++++-- crates/router/src/workflows/payment_sync.rs | 2 ++ .../src/payments/payment_attempt.rs | 20 ++++++++++++---- .../Payments - Create/request.json | 2 +- .../Recurring Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- 15 files changed, 112 insertions(+), 37 deletions(-) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 1b43177feb56..80ae283be85b 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -321,12 +321,15 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, }, - MultipleCaptureCountUpdate { - multiple_capture_count: i16, + CaptureUpdate { + amount_to_capture: Option, + multiple_capture_count: Option, updated_by: String, }, AmountToCaptureUpdate { diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index f77e75491d86..82ab9a1c02e1 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -238,12 +238,15 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, }, - MultipleCaptureCountUpdate { - multiple_capture_count: i16, + CaptureUpdate { + amount_to_capture: Option, + multiple_capture_count: Option, updated_by: String, }, AmountToCaptureUpdate { @@ -535,6 +538,8 @@ impl From for PaymentAttemptUpdateInternal { error_message, error_reason, amount_capturable, + surcharge_amount, + tax_amount, updated_by, unified_code, unified_message, @@ -547,6 +552,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, + surcharge_amount, + tax_amount, unified_code, unified_message, ..Default::default() @@ -618,12 +625,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, - PaymentAttemptUpdate::MultipleCaptureCountUpdate { + PaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, updated_by, + amount_to_capture, } => Self { - multiple_capture_count: Some(multiple_capture_count), + multiple_capture_count, updated_by, + amount_to_capture, ..Default::default() }, PaymentAttemptUpdate::AmountToCaptureUpdate { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 9c19d4eed8f6..8b20332ce5ed 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -24,7 +24,10 @@ use crate::{ payments::PaymentData, }, pii::PeekInterface, - types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId}, + types::{ + self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, + PaymentsCancelData, ResponseId, + }, utils::{OptionExt, ValueExt}, }; @@ -108,11 +111,20 @@ where } } enums::AttemptStatus::Charged => { - let captured_amount = types::Capturable::get_capture_amount(&self.request); - if Some(payment_data.payment_intent.amount) == captured_amount { - enums::AttemptStatus::Charged + let captured_amount = if self.request.is_psync() { + payment_data + .payment_attempt + .amount_to_capture + .or(Some(payment_data.payment_attempt.get_total_amount())) } else { + types::Capturable::get_capture_amount(&self.request) + }; + if Some(payment_data.payment_attempt.get_total_amount()) == captured_amount { + enums::AttemptStatus::Charged + } else if captured_amount.is_some() { enums::AttemptStatus::PartialCharged + } else { + self.status } } _ => self.status, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index ae729ff8fa25..c823fcd4937e 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -600,6 +600,29 @@ pub fn validate_request_amount_and_amount_to_capture( } } +/// if confirm = true and capture method = automatic, amount_to_capture(if provided) must be equal to amount +#[instrument(skip_all)] +pub fn validate_amount_to_capture_in_create_call_request( + request: &api_models::payments::PaymentsRequest, +) -> CustomResult<(), errors::ApiErrorResponse> { + if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic + && request.confirm.unwrap_or(false) + { + if let Some((amount_to_capture, amount)) = request.amount_to_capture.zip(request.amount) { + let amount_int: i64 = amount.into(); + utils::when(amount_to_capture != amount_int, || { + Err(report!(errors::ApiErrorResponse::PreconditionFailed { + message: "amount_to_capture must be equal to amount when confirm = true and capture_method = automatic".into() + })) + }) + } else { + Ok(()) + } + } else { + Ok(()) + } +} + #[instrument(skip_all)] pub fn validate_card_data( payment_method_data: Option, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 09e79064dc69..ef8e2b0153d4 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -251,20 +251,29 @@ impl where F: 'b + Send, { - payment_data.payment_attempt = match &payment_data.multiple_capture_data { - Some(multiple_capture_data) => db - .store + payment_data.payment_attempt = if payment_data.multiple_capture_data.is_some() + || payment_data.payment_attempt.amount_to_capture.is_some() + { + let multiple_capture_count = payment_data + .multiple_capture_data + .as_ref() + .map(|multiple_capture_data| multiple_capture_data.get_captures_count()) + .transpose()?; + let amount_to_capture = payment_data.payment_attempt.amount_to_capture; + db.store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, - storage::PaymentAttemptUpdate::MultipleCaptureCountUpdate { - multiple_capture_count: multiple_capture_data.get_captures_count()?, + storage::PaymentAttemptUpdate::CaptureUpdate { + amount_to_capture, + multiple_capture_count, updated_by: storage_scheme.to_string(), }, storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_data.payment_attempt, + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)? + } else { + payment_data.payment_attempt }; Ok((Box::new(self), payment_data)) } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 10c237cc8ab7..845915cc332c 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payment_methods}; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; @@ -279,15 +279,7 @@ impl let setup_mandate = setup_mandate.map(MandateData::from); let surcharge_details = request.surcharge_details.map(|surcharge_details| { - payment_methods::SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount: surcharge_details.surcharge_amount, - tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_details.surcharge_amount - + surcharge_details.tax_amount.unwrap_or(0), - } + surcharge_details.get_surcharge_details_object(payment_attempt.amount) }); let payment_data = PaymentData { @@ -546,6 +538,8 @@ impl ValidateRequest( } else { None }, + surcharge_amount: router_data.request.get_surcharge_amount(), + tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 3c0106206e1d..f16f7629578b 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -410,6 +410,8 @@ where status: storage_enums::AttemptStatus::Failure, error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), + surcharge_amount: None, + tax_amount: None, updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index ceeb93f69763..203d4e30bf9a 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -545,7 +545,7 @@ pub struct AccessTokenRequestData { pub trait Capturable { fn get_capture_amount(&self) -> Option { - Some(0) + None } fn get_surcharge_amount(&self) -> Option { None @@ -553,6 +553,9 @@ pub trait Capturable { fn get_tax_on_surcharge_amount(&self) -> Option { None } + fn is_psync(&self) -> bool { + false + } } impl Capturable for PaymentsAuthorizeData { @@ -591,7 +594,11 @@ impl Capturable for PaymentsCancelData {} impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} -impl Capturable for PaymentsSyncData {} +impl Capturable for PaymentsSyncData { + fn is_psync(&self) -> bool { + true + } +} pub struct AddAccessTokenResult { pub access_token_result: Result, ErrorResponse>, diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 43e327559a0c..c4b35cd6301a 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -135,6 +135,8 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { consts::REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC.to_string(), )), amount_capturable: Some(0), + surcharge_amount: None, + tax_amount: None, updated_by: merchant_account.storage_scheme.to_string(), unified_code: None, unified_message: None, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index cb74c981ea71..238a2d75087c 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1320,6 +1320,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, + tax_amount, + surcharge_amount, updated_by, unified_code, unified_message, @@ -1330,16 +1332,20 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, + surcharge_amount, + tax_amount, updated_by, unified_code, unified_message, }, - Self::MultipleCaptureCountUpdate { + Self::CaptureUpdate { multiple_capture_count, updated_by, - } => DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { + amount_to_capture, + } => DieselPaymentAttemptUpdate::CaptureUpdate { multiple_capture_count, updated_by, + amount_to_capture, }, Self::PreprocessingUpdate { status, @@ -1577,6 +1583,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, + surcharge_amount, + tax_amount, updated_by, unified_code, unified_message, @@ -1588,13 +1596,17 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, + surcharge_amount, + tax_amount, unified_code, unified_message, }, - DieselPaymentAttemptUpdate::MultipleCaptureCountUpdate { + DieselPaymentAttemptUpdate::CaptureUpdate { + amount_to_capture, multiple_capture_count, updated_by, - } => Self::MultipleCaptureCountUpdate { + } => Self::CaptureUpdate { + amount_to_capture, multiple_capture_count, updated_by, }, diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index fe57a7698926..550880583066 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json index 90c966e10f1f..304d03350584 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 6570, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json index 150139b8e104..6542d21542da 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json index 21f054843897..e37391b78b5c 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", From f8618e077065d94aa27d7153fc5ea6f93870bd81 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:25:50 +0530 Subject: [PATCH 029/443] feat: add support for 3ds and surcharge decision through routing rules (#2869) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/conditional_configs.rs | 113 +++++++ crates/api_models/src/lib.rs | 2 + .../src/surcharge_decision_configs.rs | 77 +++++ crates/router/src/core.rs | 2 + crates/router/src/core/conditional_config.rs | 204 ++++++++++++ crates/router/src/core/errors.rs | 20 ++ crates/router/src/core/payment_methods.rs | 1 + .../router/src/core/payment_methods/cards.rs | 78 ++++- .../surcharge_decision_configs.rs | 301 ++++++++++++++++++ crates/router/src/core/payments.rs | 223 +++++++++++-- .../src/core/payments/conditional_configs.rs | 118 +++++++ .../conditional_configs/transformers.rs | 22 ++ crates/router/src/core/payments/helpers.rs | 101 ++++++ crates/router/src/core/payments/operations.rs | 10 + .../payments/operations/payment_confirm.rs | 50 +-- crates/router/src/core/payments/routing.rs | 58 ++++ .../src/core/surcharge_decision_config.rs | 190 +++++++++++ crates/router/src/core/utils.rs | 2 + crates/router/src/routes/app.rs | 14 + crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/routing.rs | 168 +++++++++- .../src/types/storage/payment_attempt.rs | 10 +- crates/router_env/src/logger/types.rs | 6 + 23 files changed, 1717 insertions(+), 58 deletions(-) create mode 100644 crates/api_models/src/conditional_configs.rs create mode 100644 crates/api_models/src/surcharge_decision_configs.rs create mode 100644 crates/router/src/core/conditional_config.rs create mode 100644 crates/router/src/core/payment_methods/surcharge_decision_configs.rs create mode 100644 crates/router/src/core/payments/conditional_configs.rs create mode 100644 crates/router/src/core/payments/conditional_configs/transformers.rs create mode 100644 crates/router/src/core/surcharge_decision_config.rs diff --git a/crates/api_models/src/conditional_configs.rs b/crates/api_models/src/conditional_configs.rs new file mode 100644 index 000000000000..f8ed13421ac4 --- /dev/null +++ b/crates/api_models/src/conditional_configs.rs @@ -0,0 +1,113 @@ +use common_utils::events; +use euclid::{ + dssa::types::EuclidAnalysable, + enums, + frontend::{ + ast::Program, + dir::{DirKeyKind, DirValue, EuclidDirFilter}, + }, + types::Metadata, +}; +use serde::{Deserialize, Serialize}; + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + strum::Display, + strum::EnumVariantNames, + strum::EnumIter, + strum::EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthenticationType { + ThreeDs, + NoThreeDs, +} +impl AuthenticationType { + pub fn to_dir_value(&self) -> DirValue { + match self { + Self::ThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::ThreeDs), + Self::NoThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::NoThreeDs), + } + } +} + +impl EuclidAnalysable for AuthenticationType { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> { + let auth = self.to_string(); + + vec![( + self.to_dir_value(), + std::collections::HashMap::from_iter([( + "AUTHENTICATION_TYPE".to_string(), + serde_json::json!({ + "rule_name":rule_name, + "Authentication_type": auth, + }), + )]), + )] + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ConditionalConfigs { + pub override_3ds: Option, +} +impl EuclidDirFilter for ConditionalConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::CaptureMethod, + DirKeyKind::BillingCountry, + DirKeyKind::BusinessCountry, + ]; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DecisionManagerRecord { + pub name: String, + pub program: Program, + pub created_at: i64, + pub modified_at: i64, +} +impl events::ApiEventMetric for DecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConditionalConfigReq { + pub name: Option, + pub algorithm: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct DecisionManagerRequest { + pub name: Option, + pub program: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum DecisionManager { + DecisionManagerv0(ConditionalConfigReq), + DecisionManagerv1(DecisionManagerRequest), +} + +impl events::ApiEventMetric for DecisionManager { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} + +pub type DecisionManagerResponse = DecisionManagerRecord; diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 40faa6b3e81d..1abeff7b6ddb 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -4,6 +4,7 @@ pub mod analytics; pub mod api_keys; pub mod bank_accounts; pub mod cards_info; +pub mod conditional_configs; pub mod customers; pub mod disputes; pub mod enums; @@ -22,6 +23,7 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod surcharge_decision_configs; pub mod user; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs new file mode 100644 index 000000000000..3ebf8f42744e --- /dev/null +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -0,0 +1,77 @@ +use common_utils::{consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, events, types::Percentage}; +use euclid::frontend::{ + ast::Program, + dir::{DirKeyKind, EuclidDirFilter}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct SurchargeDetails { + pub surcharge: Surcharge, + pub tax_on_surcharge: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + Fixed(i64), + Rate(Percentage), +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct SurchargeDecisionConfigs { + pub surcharge_details: Option, +} +impl EuclidDirFilter for SurchargeDecisionConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::BillingCountry, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::PayLaterType, + DirKeyKind::WalletType, + DirKeyKind::BankTransferType, + DirKeyKind::BankRedirectType, + DirKeyKind::BankDebitType, + DirKeyKind::CryptoType, + ]; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SurchargeDecisionManagerRecord { + pub name: String, + pub merchant_surcharge_configs: MerchantSurchargeConfigs, + pub algorithm: Program, + pub created_at: i64, + pub modified_at: i64, +} + +impl events::ApiEventMetric for SurchargeDecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SurchargeDecisionConfigReq { + pub name: Option, + pub merchant_surcharge_configs: MerchantSurchargeConfigs, + pub algorithm: Option>, +} + +impl events::ApiEventMetric for SurchargeDecisionConfigReq { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct MerchantSurchargeConfigs { + pub show_surcharge_breakup_screen: Option, +} + +pub type SurchargeDecisionManagerResponse = SurchargeDecisionManagerRecord; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 8cc85eef60d6..a429cab482b4 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -3,6 +3,7 @@ pub mod api_keys; pub mod api_locking; pub mod cache; pub mod cards_info; +pub mod conditional_config; pub mod configs; pub mod customers; pub mod disputes; @@ -19,6 +20,7 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod surcharge_decision_config; #[cfg(feature = "olap")] pub mod user; pub mod utils; diff --git a/crates/router/src/core/conditional_config.rs b/crates/router/src/core/conditional_config.rs new file mode 100644 index 000000000000..e30d11ef6f2b --- /dev/null +++ b/crates/router/src/core/conditional_config.rs @@ -0,0 +1,204 @@ +use api_models::{ + conditional_configs::{DecisionManager, DecisionManagerRecord, DecisionManagerResponse}, + routing::{self}, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use diesel_models::configs; +use error_stack::{IntoReport, ResultExt}; +use euclid::frontend::ast; + +use super::routing::helpers::{ + get_payment_config_routing_id, update_merchant_active_algorithm_ref, +}; +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services::api as service_api, + types::domain, + utils::{self, OptionExt}, +}; + +pub async fn upsert_conditional_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + request: DecisionManager, +) -> RouterResponse { + let db = state.store.as_ref(); + let (name, prog) = match request { + DecisionManager::DecisionManagerv0(ccr) => { + let name = ccr.name; + + let prog = ccr + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Algorithm for config not given")?; + (name, prog) + } + DecisionManager::DecisionManagerv1(dmr) => { + let name = dmr.name; + + let prog = dmr + .program + .get_required_value("program") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "program", + }) + .attach_printable("Program for config not given")?; + (name, prog) + } + }; + let timestamp = common_utils::date_time::now_unix_timestamp(); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let key = get_payment_config_routing_id(merchant_account.merchant_id.as_str()); + let read_config_key = db.find_config_by_key(&key).await; + + ast::lowering::lower_program(prog.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + match read_config_key { + Ok(config) => { + let previous_record: DecisionManagerRecord = config + .config + .parse_struct("DecisionManagerRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Payment Config Key Not Found")?; + + let new_algo = DecisionManagerRecord { + name: previous_record.name, + program: prog, + modified_at: timestamp, + created_at: previous_record.created_at, + }; + + let serialize_updated_str = + utils::Encode::::encode_to_string_of_json(&new_algo) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize config to string")?; + + let updated_config = configs::ConfigUpdate::Update { + config: Some(serialize_updated_str), + }; + + db.update_config_by_key(&key, updated_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + + algo_id.update_conditional_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_algo)) + } + Err(e) if e.current_context().is_db_not_found() => { + let new_rec = DecisionManagerRecord { + name: name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "name", + }) + .attach_printable("name of the config not found")?, + program: prog, + modified_at: timestamp, + created_at: timestamp, + }; + + let serialized_str = + utils::Encode::::encode_to_string_of_json(&new_rec) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + let new_config = configs::ConfigNew { + key: key.clone(), + config: serialized_str, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching the config")?; + + algo_id.update_conditional_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_rec)) + } + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching payment config"), + } +} + +pub async fn delete_conditional_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key = get_payment_config_routing_id(&merchant_account.merchant_id); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|value| value.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional_config algorithm")? + .unwrap_or_default(); + algo_id.config_algo_id = None; + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update deleted algorithm ref")?; + + db.delete_config_by_key(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete routing config from DB")?; + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn retrieve_conditional_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse { + let db = state.store.as_ref(); + let algorithm_id = get_payment_config_routing_id(merchant_account.merchant_id.as_str()); + let algo_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("The conditional config was not found in the DB")?; + let record: DecisionManagerRecord = algo_config + .config + .parse_struct("ConditionalConfigRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Conditional Config Record was not found")?; + + let response = DecisionManagerRecord { + name: record.name, + program: record.program, + created_at: record.created_at, + modified_at: record.modified_at, + }; + Ok(service_api::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 03bb9a41b5b5..054f4053504e 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -375,3 +375,23 @@ pub enum RoutingError { #[error("Unable to parse metadata")] MetadataParsingError, } + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ConditionalConfigError { + #[error("failed to fetch the fallback config for the merchant")] + FallbackConfigFetchFailed, + #[error("The lock on the DSL cache is most probably poisoned")] + DslCachePoisoned, + #[error("Merchant routing algorithm not found in cache")] + CacheMiss, + #[error("Expected DSL to be saved in DB but did not find")] + DslMissingInDb, + #[error("Unable to parse DSL from JSON")] + DslParsingError, + #[error("Failed to initialize DSL backend")] + DslBackendInitError, + #[error("Error executing the DSL")] + DslExecutionError, + #[error("Error constructing the Input")] + InputConstructionError, +} diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 0628d301796e..80cec01e9166 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1,4 +1,5 @@ pub mod cards; +pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 2fe3a75d80ee..9736edc73987 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -12,6 +12,7 @@ use api_models::{ ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, + surcharge_decision_configs as api_surcharge_decision_configs, }; use common_utils::{ consts, @@ -23,6 +24,7 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; +use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; use crate::{ configs::settings, core::{ @@ -35,6 +37,7 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, + utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -1527,6 +1530,21 @@ pub async fn list_payment_methods( }); } + let merchant_surcharge_configs = + if let Some((attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent) { + Box::pin(call_surcharge_decision_management( + state, + &merchant_account, + attempt, + payment_intent, + billing_address, + &mut payment_method_responses, + )) + .await? + } else { + api_surcharge_decision_configs::MerchantSurchargeConfigs::default() + }; + Ok(services::ApplicationResponse::Json( api::PaymentMethodListResponse { redirect_url: merchant_account.return_url, @@ -1558,11 +1576,69 @@ pub async fn list_payment_methods( } }, ), - show_surcharge_breakup_screen: false, + show_surcharge_breakup_screen: merchant_surcharge_configs + .show_surcharge_breakup_screen + .unwrap_or_default(), }, )) } +pub async fn call_surcharge_decision_management( + state: routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + billing_address: Option, + response_payment_method_types: &mut [ResponsePaymentMethodsEnabled], +) -> errors::RouterResult { + if payment_attempt.surcharge_amount.is_some() { + Ok(api_surcharge_decision_configs::MerchantSurchargeConfigs::default()) + } else { + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let (surcharge_results, merchant_sucharge_configs) = + perform_surcharge_decision_management_for_payment_method_list( + &state, + algorithm_ref, + payment_attempt, + &payment_intent, + billing_address.as_ref().map(Into::into), + response_payment_method_types, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + persist_individual_surcharge_details_in_redis( + &state, + merchant_account, + &surcharge_results, + ) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); + } + Ok(merchant_sucharge_configs) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs new file mode 100644 index 000000000000..9a65ec76f2a5 --- /dev/null +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -0,0 +1,301 @@ +use api_models::{ + payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata}, + payments::Address, + routing, + surcharge_decision_configs::{ + self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails, + }, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use error_stack::{self, IntoReport, ResultExt}; +use euclid::{ + backend, + backend::{inputs as dsl_inputs, EuclidBackend}, +}; +use router_env::{instrument, tracing}; + +use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage}; +static CONF_CACHE: StaticCache = StaticCache::new(); +use crate::{ + core::{ + errors::ConditionalConfigError as ConfigError, + payments::{ + conditional_configs::ConditionalConfigResult, routing::make_dsl_input_for_surcharge, + }, + }, + AppState, +}; + +struct VirInterpreterBackendCacheWrapper { + cached_alogorith: backend::VirInterpreterBackend, + merchant_surcharge_configs: surcharge_decision_configs::MerchantSurchargeConfigs, +} + +impl TryFrom for VirInterpreterBackendCacheWrapper { + type Error = error_stack::Report; + + fn try_from(value: SurchargeDecisionManagerRecord) -> Result { + let cached_alogorith = backend::VirInterpreterBackend::with_program(value.algorithm) + .into_report() + .change_context(ConfigError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + let merchant_surcharge_configs = value.merchant_surcharge_configs; + Ok(Self { + cached_alogorith, + merchant_surcharge_configs, + }) + } +} + +pub async fn perform_surcharge_decision_management_for_payment_method_list( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + billing_address: Option
, + response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], +) -> ConditionalConfigResult<( + SurchargeMetadata, + surcharge_decision_configs::MerchantSurchargeConfigs, +)> { + let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(( + surcharge_metadata, + surcharge_decision_configs::MerchantSurchargeConfigs::default(), + )); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = + make_dsl_input_for_surcharge(payment_attempt, payment_intent, billing_address) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + let merchant_surcharge_configs = cached_algo.merchant_surcharge_configs.clone(); + + for payment_methods_enabled in response_payment_method_types.iter_mut() { + for payment_method_type_response in + &mut payment_methods_enabled.payment_method_types.iter_mut() + { + let payment_method_type = payment_method_type_response.payment_method_type; + backend_input.payment_method.payment_method_type = Some(payment_method_type); + backend_input.payment_method.payment_method = + Some(payment_methods_enabled.payment_method); + + if let Some(card_network_list) = &mut payment_method_type_response.card_networks { + for card_network_type in card_network_list.iter_mut() { + backend_input.payment_method.card_network = + Some(card_network_type.card_network.clone()); + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + card_network_type.surcharge_details = surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_response(surcharge_details, payment_attempt).map( + |surcharge_details_response| { + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + Some(&card_network_type.card_network), + surcharge_details_response.clone(), + ); + surcharge_details_response + }, + ) + }) + .transpose()?; + } + } else { + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + payment_method_type_response.surcharge_details = surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_response(surcharge_details, payment_attempt).map( + |surcharge_details_response| { + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + None, + surcharge_details_response.clone(), + ); + surcharge_details_response + }, + ) + }) + .transpose()?; + } + } + } + Ok((surcharge_metadata, merchant_surcharge_configs)) +} + +pub async fn perform_surcharge_decision_management_for_session_flow( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_data: &mut PaymentData, + payment_method_type_list: &Vec, +) -> ConditionalConfigResult +where + O: Send + Clone, +{ + let mut surcharge_metadata = + SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(surcharge_metadata); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_data.payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = make_dsl_input_for_surcharge( + &payment_data.payment_attempt, + &payment_data.payment_intent, + payment_data.address.billing.clone(), + ) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + for payment_method_type in payment_method_type_list { + backend_input.payment_method.payment_method_type = Some(*payment_method_type); + // in case of session flow, payment_method will always be wallet + backend_input.payment_method.payment_method = Some(payment_method_type.to_owned().into()); + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details) = surcharge_output.surcharge_details { + let surcharge_details_response = + get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?; + surcharge_metadata.insert_surcharge_details( + &payment_method_type.to_owned().into(), + payment_method_type, + None, + surcharge_details_response, + ); + } + } + Ok(surcharge_metadata) +} + +fn get_surcharge_details_response( + surcharge_details: SurchargeDetails, + payment_attempt: &oss_storage::PaymentAttempt, +) -> ConditionalConfigResult { + let surcharge_amount = match surcharge_details.surcharge.clone() { + surcharge_decision_configs::Surcharge::Fixed(value) => value, + surcharge_decision_configs::Surcharge::Rate(percentage) => percentage + .apply_and_ceil_result(payment_attempt.amount) + .change_context(ConfigError::DslExecutionError) + .attach_printable("Failed to Calculate surcharge amount by applying percentage")?, + }; + let tax_on_surcharge_amount = surcharge_details + .tax_on_surcharge + .clone() + .map(|tax_on_surcharge| { + tax_on_surcharge + .apply_and_ceil_result(surcharge_amount) + .change_context(ConfigError::DslExecutionError) + .attach_printable("Failed to Calculate tax amount") + }) + .transpose()? + .unwrap_or(0); + Ok(SurchargeDetailsResponse { + surcharge: match surcharge_details.surcharge { + surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => { + payment_methods::Surcharge::Fixed(surcharge_amount) + } + surcharge_decision_configs::Surcharge::Rate(percentage) => { + payment_methods::Surcharge::Rate(percentage) + } + }, + tax_on_surcharge: surcharge_details.tax_on_surcharge, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + }) +} + +#[instrument(skip_all)] +pub async fn ensure_algorithm_cached( + store: &dyn StorageInterface, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, +) -> ConditionalConfigResult { + let key = format!("surcharge_dsl_{merchant_id}"); + let present = CONF_CACHE + .present(&key) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + let expired = CONF_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + + if !present || expired { + refresh_surcharge_algorithm_cache(store, key.clone(), algorithm_id, timestamp).await? + } + Ok(key) +} + +#[instrument(skip_all)] +pub async fn refresh_surcharge_algorithm_cache( + store: &dyn StorageInterface, + key: String, + algorithm_id: &str, + timestamp: i64, +) -> ConditionalConfigResult<()> { + let config = store + .find_config_by_key(algorithm_id) + .await + .change_context(ConfigError::DslMissingInDb) + .attach_printable("Error parsing DSL from config")?; + let record: SurchargeDecisionManagerRecord = config + .config + .parse_struct("Program") + .change_context(ConfigError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + let value_to_cache = VirInterpreterBackendCacheWrapper::try_from(record)?; + CONF_CACHE + .save(key, value_to_cache, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + Ok(()) +} + +pub fn execute_dsl_and_get_conditional_config( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { + let routing_output = interpreter + .execute(backend_input) + .map(|out| out.connector_selection) + .into_report() + .change_context(ConfigError::DslExecutionError)?; + Ok(routing_output) +} diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 0259c48ee827..8c13b05836f1 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1,4 +1,5 @@ pub mod access_token; +pub mod conditional_configs; pub mod customers; pub mod flows; pub mod helpers; @@ -13,9 +14,9 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; use api_models::{ - enums, + self, enums, payment_methods::{Surcharge, SurchargeDetailsResponse}, - payments::HeaderPayload, + payments::{self, HeaderPayload}, }; use common_utils::{ext_traits::AsyncExt, pii}; use data_models::mandates::MandateData; @@ -24,6 +25,7 @@ use error_stack::{IntoReport, ResultExt}; use futures::future::join_all; use helpers::ApplePayData; use masking::Secret; +use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; #[cfg(feature = "olap")] use router_types::transformers::ForeignFrom; @@ -35,11 +37,15 @@ pub use self::operations::{ PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, }; use self::{ + conditional_configs::perform_decision_management, flows::{ConstructFlowSpecificData, Feature}, + helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, }; -use super::errors::StorageErrorExt; +use super::{ + errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, +}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -55,8 +61,8 @@ use crate::{ self as router_types, api::{self, ConnectorCallType}, domain, - storage::{self, enums as storage_enums}, - transformers::ForeignTryInto, + storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt}, + transformers::{ForeignInto, ForeignTryInto}, }, utils::{ add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt, @@ -141,6 +147,8 @@ where .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; + call_decision_manager(state, &merchant_account, &mut payment_data).await?; + let connector = get_connector_choice( &operation, state, @@ -167,6 +175,10 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { + operation + .to_domain()? + .populate_payment_data(state, &mut payment_data, &req, &merchant_account) + .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -294,8 +306,14 @@ where } api::ConnectorCallType::SessionMultiple(connectors) => { - let session_surcharge_data = - get_session_surcharge_data(&payment_data.payment_attempt); + let session_surcharge_details = + call_surcharge_decision_management_for_session_flow( + state, + &merchant_account, + &mut payment_data, + &connectors, + ) + .await?; call_multiple_connectors_service( state, &merchant_account, @@ -304,7 +322,7 @@ where &operation, payment_data, &customer, - session_surcharge_data, + session_surcharge_details, ) .await? } @@ -348,6 +366,123 @@ where )) } +#[instrument(skip_all)] +pub async fn call_decision_manager( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, +) -> RouterResult<()> +where + O: Send + Clone, +{ + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let output = perform_decision_management( + state, + algorithm_ref, + merchant_account.merchant_id.as_str(), + payment_data, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional config")?; + payment_data.payment_attempt.authentication_type = payment_data + .payment_attempt + .authentication_type + .or(output.override_3ds.map(ForeignInto::foreign_into)) + .or(Some(storage_enums::AuthenticationType::NoThreeDs)); + Ok(()) +} + +#[instrument(skip_all)] +async fn populate_surcharge_details( + state: &AppState, + payment_data: &mut PaymentData, + request: &payments::PaymentsRequest, +) -> RouterResult<()> +where + F: Send + Clone, +{ + if payment_data + .payment_intent + .surcharge_applicable + .unwrap_or(false) + { + let payment_method_data = request + .payment_method_data + .clone() + .get_required_value("payment_method_data")?; + let (payment_method, payment_method_type, card_network) = + get_key_params_for_surcharge_details(payment_method_data)?; + + let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis( + state, + &payment_method, + &payment_method_type, + card_network, + &payment_data.payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => Some(surcharge_details), + Err(err) if err.current_context() == &RedisError::NotFound => None, + Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, + }; + + let request_surcharge_details = request.surcharge_details; + + match (request_surcharge_details, calculated_surcharge_details) { + (Some(request_surcharge_details), Some(calculated_surcharge_details)) => { + if calculated_surcharge_details + .is_request_surcharge_matching(request_surcharge_details) + { + payment_data.surcharge_details = Some(calculated_surcharge_details); + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), + } + .into()); + } + } + (None, Some(_calculated_surcharge_details)) => { + return Err(errors::ApiErrorResponse::MissingRequiredField { + field_name: "surcharge_details", + } + .into()); + } + (Some(request_surcharge_details), None) => { + if request_surcharge_details.is_surcharge_zero() { + return Ok(()); + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), + } + .into()); + } + } + (None, None) => return Ok(()), + }; + } else { + let surcharge_details = + payment_data + .payment_attempt + .get_surcharge_details() + .map(|surcharge_details| { + surcharge_details + .get_surcharge_details_object(payment_data.payment_attempt.amount) + }); + payment_data.surcharge_details = surcharge_details; + } + Ok(()) +} + #[inline] pub fn get_connector_data( connectors: &mut IntoIter, @@ -359,20 +494,66 @@ pub fn get_connector_data( .attach_printable("Connector not found in connectors iterator") } -pub fn get_session_surcharge_data( - payment_attempt: &data_models::payments::payment_attempt::PaymentAttempt, -) -> Option { - payment_attempt.surcharge_amount.map(|surcharge_amount| { - let tax_on_surcharge_amount = payment_attempt.tax_amount.unwrap_or(0); - let final_amount = payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; - api::SessionSurchargeDetails::PreDetermined(SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - final_amount, +#[instrument(skip_all)] +pub async fn call_surcharge_decision_management_for_session_flow( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, + session_connector_data: &[api::SessionConnectorData], +) -> RouterResult> +where + O: Send + Clone + Sync, +{ + if let Some(surcharge_amount) = payment_data.payment_attempt.surcharge_amount { + let tax_on_surcharge_amount = payment_data.payment_attempt.tax_amount.unwrap_or(0); + let final_amount = + payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; + Ok(Some(api::SessionSurchargeDetails::PreDetermined( + SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount, + }, + ))) + } else { + let payment_method_type_list = session_connector_data + .iter() + .map(|session_connector_data| session_connector_data.payment_method_type) + .collect(); + let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = + surcharge_decision_configs::perform_surcharge_decision_management_for_session_flow( + state, + algorithm_ref, + payment_data, + &payment_method_type_list, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + + core_utils::persist_individual_surcharge_details_in_redis( + state, + merchant_account, + &surcharge_results, + ) + .await?; + + Ok(if surcharge_results.is_empty_result() { + None + } else { + Some(api::SessionSurchargeDetails::Calculated(surcharge_results)) }) - }) + } } #[allow(clippy::too_many_arguments)] pub async fn payments_core( diff --git a/crates/router/src/core/payments/conditional_configs.rs b/crates/router/src/core/payments/conditional_configs.rs new file mode 100644 index 000000000000..bf1f43e2b0f9 --- /dev/null +++ b/crates/router/src/core/payments/conditional_configs.rs @@ -0,0 +1,118 @@ +mod transformers; + +use api_models::{ + conditional_configs::{ConditionalConfigs, DecisionManagerRecord}, + routing, +}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use error_stack::{IntoReport, ResultExt}; +use euclid::backend::{self, inputs as dsl_inputs, EuclidBackend}; +use router_env::{instrument, tracing}; + +use super::routing::make_dsl_input; +use crate::{ + core::{errors, errors::ConditionalConfigError as ConfigError, payments}, + routes, +}; + +static CONF_CACHE: StaticCache> = + StaticCache::new(); +pub type ConditionalConfigResult = errors::CustomResult; + +#[instrument(skip_all)] +pub async fn perform_decision_management( + state: &routes::AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + merchant_id: &str, + payment_data: &mut payments::PaymentData, +) -> ConditionalConfigResult { + let algorithm_id = if let Some(id) = algorithm_ref.config_algo_id { + id + } else { + return Ok(ConditionalConfigs::default()); + }; + + let key = ensure_algorithm_cached( + state, + merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let backend_input = + make_dsl_input(payment_data).change_context(ConfigError::InputConstructionError)?; + let interpreter = cached_algo.as_ref(); + execute_dsl_and_get_conditional_config(backend_input, interpreter).await +} + +#[instrument(skip_all)] +pub async fn ensure_algorithm_cached( + state: &routes::AppState, + merchant_id: &str, + timestamp: i64, + algorithm_id: &str, +) -> ConditionalConfigResult { + let key = format!("dsl_{merchant_id}"); + let present = CONF_CACHE + .present(&key) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presece of DSL")?; + let expired = CONF_CACHE + .expired(&key, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error checking presence of DSL")?; + if !present || expired { + refresh_routing_cache(state, key.clone(), algorithm_id, timestamp).await?; + }; + Ok(key) +} + +#[instrument(skip_all)] +pub async fn refresh_routing_cache( + state: &routes::AppState, + key: String, + algorithm_id: &str, + timestamp: i64, +) -> ConditionalConfigResult<()> { + let config = state + .store + .find_config_by_key(algorithm_id) + .await + .change_context(ConfigError::DslMissingInDb) + .attach_printable("Error parsing DSL from config")?; + let rec: DecisionManagerRecord = config + .config + .parse_struct("Program") + .change_context(ConfigError::DslParsingError) + .attach_printable("Error parsing routing algorithm from configs")?; + let interpreter: backend::VirInterpreterBackend = + backend::VirInterpreterBackend::with_program(rec.program) + .into_report() + .change_context(ConfigError::DslBackendInitError) + .attach_printable("Error initializing DSL interpreter backend")?; + CONF_CACHE + .save(key, interpreter, timestamp) + .into_report() + .change_context(ConfigError::DslCachePoisoned) + .attach_printable("Error saving DSL to cache")?; + Ok(()) +} + +pub async fn execute_dsl_and_get_conditional_config( + backend_input: dsl_inputs::BackendInput, + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { + let routing_output = interpreter + .execute(backend_input) + .map(|out| out.connector_selection) + .into_report() + .change_context(ConfigError::DslExecutionError)?; + Ok(routing_output) +} diff --git a/crates/router/src/core/payments/conditional_configs/transformers.rs b/crates/router/src/core/payments/conditional_configs/transformers.rs new file mode 100644 index 000000000000..023bd65dcf41 --- /dev/null +++ b/crates/router/src/core/payments/conditional_configs/transformers.rs @@ -0,0 +1,22 @@ +use api_models::{self, conditional_configs}; +use diesel_models::enums as storage_enums; +use euclid::enums as dsl_enums; + +use crate::types::transformers::ForeignFrom; +impl ForeignFrom for conditional_configs::AuthenticationType { + fn foreign_from(from: dsl_enums::AuthenticationType) -> Self { + match from { + dsl_enums::AuthenticationType::ThreeDs => Self::ThreeDs, + dsl_enums::AuthenticationType::NoThreeDs => Self::NoThreeDs, + } + } +} + +impl ForeignFrom for storage_enums::AuthenticationType { + fn foreign_from(from: conditional_configs::AuthenticationType) -> Self { + match from { + conditional_configs::AuthenticationType::ThreeDs => Self::ThreeDs, + conditional_configs::AuthenticationType::NoThreeDs => Self::NoThreeDs, + } + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c823fcd4937e..4d8daa1fe69d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use api_models::payments::GetPaymentMethodType; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -3516,6 +3517,106 @@ impl ApplePayData { } } +pub fn get_key_params_for_surcharge_details( + payment_method_data: api_models::payments::PaymentMethodData, +) -> RouterResult<( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, +)> { + match payment_method_data { + api_models::payments::PaymentMethodData::Card(card) => { + let card_type = card + .card_type + .get_required_value("payment_method_data.card.card_type")?; + let card_network = card + .card_network + .get_required_value("payment_method_data.card.card_network")?; + match card_type.to_lowercase().as_str() { + "credit" => Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + Some(card_network), + )), + "debit" => Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Debit, + Some(card_network), + )), + _ => { + logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable"); + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data.card.card_type", + } + .into()) + } + } + } + api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( + common_enums::PaymentMethod::CardRedirect, + card_redirect_data.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Wallet(wallet) => Ok(( + common_enums::PaymentMethod::Wallet, + wallet.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok(( + common_enums::PaymentMethod::PayLater, + pay_later.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Ok(( + common_enums::PaymentMethod::BankRedirect, + bank_redirect.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Ok(( + common_enums::PaymentMethod::BankDebit, + bank_debit.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Ok(( + common_enums::PaymentMethod::BankTransfer, + bank_transfer.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::Crypto(crypto) => Ok(( + common_enums::PaymentMethod::Crypto, + crypto.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::MandatePayment => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + api_models::payments::PaymentMethodData::Reward => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } + api_models::payments::PaymentMethodData::Upi(_) => Ok(( + common_enums::PaymentMethod::Upi, + common_enums::PaymentMethodType::UpiCollect, + None, + )), + api_models::payments::PaymentMethodData::Voucher(voucher) => Ok(( + common_enums::PaymentMethod::Voucher, + voucher.get_payment_method_type(), + None, + )), + api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok(( + common_enums::PaymentMethod::GiftCard, + gift_card.get_payment_method_type(), + None, + )), + } +} + pub fn validate_payment_link_request( payment_link_object: &api_models::payments::PaymentLinkObject, confirm: Option, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 6f01c653084f..809c9e925de0 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -152,6 +152,16 @@ pub trait Domain: Send + Sync { payment_intent: &storage::PaymentIntent, mechant_key_store: &domain::MerchantKeyStore, ) -> CustomResult; + + async fn populate_payment_data<'a>( + &'a self, + _state: &AppState, + _payment_data: &mut PaymentData, + _request: &R, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 8b4f91b63a2e..e85531050529 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,9 +1,6 @@ use std::marker::PhantomData; -use api_models::{ - enums::FrmSuggestion, - payment_methods::{self, SurchargeDetailsResponse}, -}; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; @@ -18,7 +15,10 @@ use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{ + self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, + PaymentData, + }, utils::{self as core_utils, get_individual_surcharge_detail_from_redis}, }, db::StorageInterface, @@ -430,11 +430,6 @@ impl ) .await?; - let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt( - request, - &payment_attempt, - ); - let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -465,7 +460,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, - surcharge_details, + surcharge_details: None, frm_message: None, payment_link_data: None, }; @@ -574,6 +569,17 @@ impl Domain( + &'a self, + state: &AppState, + payment_data: &mut PaymentData, + request: &api::PaymentsRequest, + _merchant_account: &domain::MerchantAccount, + ) -> CustomResult<(), errors::ApiErrorResponse> { + populate_surcharge_details(state, payment_data, request).await + } } #[async_trait] @@ -921,26 +927,4 @@ impl PaymentConfirm { _ => Ok(()), } } - - fn get_surcharge_details_from_payment_request_or_payment_attempt( - payment_request: &api::PaymentsRequest, - payment_attempt: &storage::PaymentAttempt, - ) -> Option { - payment_request - .surcharge_details - .map(|surcharge_details| { - surcharge_details.get_surcharge_details_object(payment_attempt.amount) - }) // if not passed in confirm request, look inside payment_attempt - .or(payment_attempt - .surcharge_amount - .map(|surcharge_amount| SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_amount - + payment_attempt.tax_amount.unwrap_or(0), - })) - } } diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 3b89d4e38e4e..841b48b9444a 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -9,6 +9,7 @@ use std::{ use api_models::{ admin as admin_api, enums::{self as api_enums, CountryAlpha2}, + payments::Address, routing::ConnectorSelection, }; use common_utils::static_cache::StaticCache; @@ -996,3 +997,60 @@ async fn perform_session_routing_for_pm_type( Ok(final_choice) } + +pub fn make_dsl_input_for_surcharge( + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + billing_address: Option
, +) -> RoutingResult { + let mandate_data = dsl_inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }; + let payment_input = dsl_inputs::PaymentInput { + amount: payment_attempt.amount, + // currency is always populated in payment_attempt during payment create + currency: payment_attempt + .currency + .get_required_value("currency") + .change_context(errors::RoutingError::DslMissingRequiredField { + field_name: "currency".to_string(), + })?, + authentication_type: payment_attempt.authentication_type, + card_bin: None, + capture_method: payment_attempt.capture_method, + business_country: payment_intent + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: billing_address + .and_then(|bic| bic.address) + .and_then(|add| add.country) + .map(api_enums::Country::from_alpha2), + business_label: payment_intent.business_label.clone(), + setup_future_usage: payment_intent.setup_future_usage, + }; + let metadata = payment_intent + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: None, + payment_method_type: None, + card_network: None, + }; + let backend_input = dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: mandate_data, + }; + Ok(backend_input) +} diff --git a/crates/router/src/core/surcharge_decision_config.rs b/crates/router/src/core/surcharge_decision_config.rs new file mode 100644 index 000000000000..82615aef2845 --- /dev/null +++ b/crates/router/src/core/surcharge_decision_config.rs @@ -0,0 +1,190 @@ +use api_models::{ + routing::{self}, + surcharge_decision_configs::{ + SurchargeDecisionConfigReq, SurchargeDecisionManagerRecord, + SurchargeDecisionManagerResponse, + }, +}; +use common_utils::ext_traits::{StringExt, ValueExt}; +use diesel_models::configs; +use error_stack::{IntoReport, ResultExt}; +use euclid::frontend::ast; + +use super::routing::helpers::{ + get_payment_method_surcharge_routing_id, update_merchant_active_algorithm_ref, +}; +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services::api as service_api, + types::domain, + utils::{self, OptionExt}, +}; + +pub async fn upsert_surcharge_decision_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + request: SurchargeDecisionConfigReq, +) -> RouterResponse { + let db = state.store.as_ref(); + let name = request.name; + + let program = request + .algorithm + .get_required_value("algorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "algorithm", + }) + .attach_printable("Program for config not given")?; + let merchant_surcharge_configs = request.merchant_surcharge_configs; + + let timestamp = common_utils::date_time::now_unix_timestamp(); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + + let key = get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str()); + let read_config_key = db.find_config_by_key(&key).await; + + ast::lowering::lower_program(program.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + match read_config_key { + Ok(config) => { + let previous_record: SurchargeDecisionManagerRecord = config + .config + .parse_struct("SurchargeDecisionManagerRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Payment Config Key Not Found")?; + + let new_algo = SurchargeDecisionManagerRecord { + name: name.unwrap_or(previous_record.name), + algorithm: program, + modified_at: timestamp, + created_at: previous_record.created_at, + merchant_surcharge_configs, + }; + + let serialize_updated_str = + utils::Encode::::encode_to_string_of_json( + &new_algo, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize config to string")?; + + let updated_config = configs::ConfigUpdate::Update { + config: Some(serialize_updated_str), + }; + + db.update_config_by_key(&key, updated_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + + algo_id.update_surcharge_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_algo)) + } + Err(e) if e.current_context().is_db_not_found() => { + let new_rec = SurchargeDecisionManagerRecord { + name: name + .get_required_value("name") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "name", + }) + .attach_printable("name of the config not found")?, + algorithm: program, + merchant_surcharge_configs, + modified_at: timestamp, + created_at: timestamp, + }; + + let serialized_str = + utils::Encode::::encode_to_string_of_json(&new_rec) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error serializing the config")?; + let new_config = configs::ConfigNew { + key: key.clone(), + config: serialized_str, + }; + + db.insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching the config")?; + + algo_id.update_surcharge_config_id(key); + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update routing algorithm ref")?; + + Ok(service_api::ApplicationResponse::Json(new_rec)) + } + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error fetching payment config"), + } +} + +pub async fn delete_surcharge_decision_config( + state: AppState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key = get_payment_method_surcharge_routing_id(&merchant_account.merchant_id); + let mut algo_id: routing::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|value| value.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the surcharge conditional_config algorithm")? + .unwrap_or_default(); + algo_id.surcharge_config_algo_id = None; + update_merchant_active_algorithm_ref(db, &key_store, algo_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update deleted algorithm ref")?; + + db.delete_config_by_key(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to delete routing config from DB")?; + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn retrieve_surcharge_decision_config( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse { + let db = state.store.as_ref(); + let algorithm_id = + get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str()); + let algo_config = db + .find_config_by_key(&algorithm_id) + .await + .change_context(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("The surcharge conditional config was not found in the DB")?; + let record: SurchargeDecisionManagerRecord = algo_config + .config + .parse_struct("SurchargeDecisionConfigsRecord") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Surcharge Decision Config Record was not found")?; + Ok(service_api::ApplicationResponse::Json(record)) +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 5ffc85fe6709..5207e4ba8079 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1070,6 +1070,7 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } +#[instrument(skip_all)] pub async fn persist_individual_surcharge_details_in_redis( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1109,6 +1110,7 @@ pub async fn persist_individual_surcharge_details_in_redis( Ok(()) } +#[instrument(skip_all)] pub async fn get_individual_surcharge_detail_from_redis( state: &AppState, payment_method: &euclid_enums::PaymentMethod, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 070f1eb29bf8..79801e8e64f0 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -325,6 +325,20 @@ impl Routing { web::resource("/deactivate") .route(web::post().to(cloud_routing::routing_unlink_config)), ) + .service( + web::resource("/decision") + .route(web::put().to(cloud_routing::upsert_decision_manager_config)) + .route(web::get().to(cloud_routing::retrieve_decision_manager_config)) + .route(web::delete().to(cloud_routing::delete_decision_manager_config)), + ) + .service( + web::resource("/decision/surcharge") + .route(web::put().to(cloud_routing::upsert_surcharge_decision_manager_config)) + .route(web::get().to(cloud_routing::retrieve_surcharge_decision_manager_config)) + .route( + web::delete().to(cloud_routing::delete_surcharge_decision_manager_config), + ), + ) .service( web::resource("/{algorithm_id}") .route(web::get().to(cloud_routing::routing_retrieve_config)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c093523d455a..84b00867b98d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -46,7 +46,10 @@ impl From for ApiIdentifier { | Flow::RoutingRetrieveDictionary | Flow::RoutingUpdateConfig | Flow::RoutingUpdateDefaultConfig - | Flow::RoutingDeleteConfig => Self::Routing, + | Flow::RoutingDeleteConfig + | Flow::DecisionManagerDeleteConfig + | Flow::DecisionManagerRetrieveConfig + | Flow::DecisionManagerUpsertConfig => Self::Routing, Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 606111a88818..1d2549bb047a 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -12,7 +12,7 @@ use router_env::{ }; use crate::{ - core::{api_locking, routing}, + core::{api_locking, conditional_config, routing, surcharge_decision_config}, routes::AppState, services::{api as oss_api, authentication as auth}, }; @@ -248,6 +248,172 @@ pub async fn routing_retrieve_default_config( .await } +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn upsert_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision| { + surcharge_decision_config::upsert_surcharge_decision_config( + state, + auth.key_store, + auth.merchant_account, + update_decision, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn delete_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerDeleteConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, ()| { + surcharge_decision_config::delete_surcharge_decision_config( + state, + auth.key_store, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_surcharge_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + surcharge_decision_config::retrieve_surcharge_decision_config( + state, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn upsert_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision| { + conditional_config::upsert_conditional_config( + state, + auth.key_store, + auth.merchant_account, + update_decision, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn delete_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerDeleteConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, ()| { + conditional_config::delete_conditional_config( + state, + auth.key_store, + auth.merchant_account, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + conditional_config::retrieve_conditional_config(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + #[cfg(feature = "olap")] #[instrument(skip_all)] pub async fn routing_retrieve_linked_config( diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index a4fbcb022005..f94d06997ca9 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -17,6 +17,7 @@ pub trait PaymentAttemptExt { fn get_next_capture_id(&self) -> String; fn get_total_amount(&self) -> i64; + fn get_surcharge_details(&self) -> Option; } impl PaymentAttemptExt for PaymentAttempt { @@ -58,7 +59,14 @@ impl PaymentAttemptExt for PaymentAttempt { let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1; format!("{}_{}", self.attempt_id.clone(), next_sequence_number) } - + fn get_surcharge_details(&self) -> Option { + self.surcharge_amount.map(|surcharge_amount| { + api_models::payments::RequestSurchargeDetails { + surcharge_amount, + tax_amount: self.tax_amount, + } + }) + } fn get_total_amount(&self) -> i64 { self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 3bfd1ef7d9f8..f6d61f550840 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -247,6 +247,12 @@ pub enum Flow { GsmRuleDelete, /// User connect account UserConnectAccount, + /// Upsert Decision Manager Config + DecisionManagerUpsertConfig, + /// Delete Decision Manager Config + DecisionManagerDeleteConfig, + /// Retrieve Decision Manager Config + DecisionManagerRetrieveConfig, } /// From e66ccde4cf6d055b7d02c5e982d2e09364845602 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:33:06 +0530 Subject: [PATCH 030/443] fix(mca): Change the check for `disabled` field in mca create and update (#2938) --- crates/router/src/core/admin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 3a0c938c32b4..107e8f8859d6 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1767,7 +1767,7 @@ pub fn validate_status_and_disabled( }; let disabled = match (disabled, connector_status) { - (Some(true), common_enums::ConnectorStatus::Inactive) => { + (Some(false), common_enums::ConnectorStatus::Inactive) => { return Err(errors::ApiErrorResponse::InvalidRequestData { message: "Connector cannot be enabled when connector_status is inactive or when using TemporaryAuth" .to_string(), From 15a255ea60dffad9e4cf20d642636028c27c7c00 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 21 Nov 2023 21:22:50 +0530 Subject: [PATCH 031/443] feat(connector): [Prophetpay] Save card token for Refund and remove Void flow (#2927) --- crates/router/src/connector/prophetpay.rs | 47 +-- .../src/connector/prophetpay/transformers.rs | 384 ++++++++++++------ 2 files changed, 272 insertions(+), 159 deletions(-) diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index e5ebe6331ba2..efe87bcefd9f 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -107,16 +107,15 @@ impl ConnectorCommon for Prophetpay { &self, res: Response, ) -> CustomResult { - let response: prophetpay::ProphetpayErrorResponse = res + let response: serde_json::Value = res .response - .parse_struct("ProphetpayErrorResponse") + .parse_struct("ProphetPayErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { status_code: res.status_code, - code: response.status.to_string(), - message: response.title, - reason: Some(response.errors.to_string()), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(response.to_string()), attempt_status: None, }) } @@ -324,7 +323,7 @@ impl where types::PaymentsResponseData: Clone, { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpayCompleteAuthResponse = res .response .parse_struct("prophetpay ProphetpayResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -407,9 +406,9 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpaySyncResponse = res .response - .parse_struct("prophetpay ProphetpayResponse") + .parse_struct("prophetpay PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -431,9 +430,12 @@ impl ConnectorIntegration for Prophetpay { + /* fn get_headers( &self, req: &types::PaymentsCancelRouterData, @@ -471,33 +473,25 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Get) - .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( - self, req, connectors, - )?) - .build(), - )) + Err(errors::ConnectorError::NotImplemented("Void flow not implemented".to_string()).into()) } + /* fn handle_response( &self, data: &types::PaymentsCancelRouterData, res: Response, ) -> CustomResult { - let response: prophetpay::ProphetpayResponse = res + let response: prophetpay::ProphetpayVoidResponse = res .response - .parse_struct("prophetpay ProphetpayResponse") + .parse_struct("prophetpay PaymentsCancelResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -512,6 +506,7 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + */ } impl ConnectorIntegration @@ -652,7 +647,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) + .method(services::Method::Post) .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) @@ -668,7 +663,7 @@ impl ConnectorIntegration CustomResult { - let response: prophetpay::ProphetpayRefundResponse = res + let response: prophetpay::ProphetpayRefundSyncResponse = res .response .parse_struct("prophetpay ProphetpayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index 74071d5b85cb..b8cf3e3a1f5b 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils, + connector::utils::{self, to_connector_meta}, core::errors, services, types::{self, api, storage::enums}, @@ -159,7 +159,11 @@ impl TryFrom<&ProphetpayRouterData<&types::PaymentsAuthorizeRouterData>> ), } } else { - Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) + Err(errors::ConnectorError::CurrencyNotSupported { + message: item.router_data.request.currency.to_string(), + connector: "Prophetpay", + } + .into()) } } } @@ -266,10 +270,7 @@ impl TryFrom<&ProphetpayRouterData<&types::PaymentsCompleteAuthorizeRouterData>> Ok(Self { amount: item.amount.to_owned(), ref_info: item.router_data.connector_request_reference_id.to_owned(), - inquiry_reference: format!( - "inquiry_{}", - item.router_data.connector_request_reference_id - ), + inquiry_reference: item.router_data.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Charge), card_token, @@ -346,8 +347,8 @@ impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(Self { transaction_id, - ref_info: item.attempt_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.attempt_id), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) @@ -355,66 +356,170 @@ impl TryFrom<&types::PaymentsSyncRouterData> for ProphetpaySyncRequest { } #[derive(Debug, Clone, Deserialize)] -pub enum ProphetpayPaymentStatus { - Success, - #[serde(rename = "Transaction Approved")] - Charged, - Failure, - #[serde(rename = "Transaction Voided")] - Voided, - #[serde(rename = "Requires a card on file.")] - CardTokenNotFound, - #[serde(rename = "RefInfo and InquiryReference are duplicated")] - DuplicateValue, - #[serde(rename = "Profile is missing")] - MissingProfile, - #[serde(rename = "RefInfo is empty.")] - EmptyRef, -} - -impl From for enums::AttemptStatus { - fn from(item: ProphetpayPaymentStatus) -> Self { - match item { - ProphetpayPaymentStatus::Success | ProphetpayPaymentStatus::Charged => Self::Charged, - ProphetpayPaymentStatus::Failure - | ProphetpayPaymentStatus::CardTokenNotFound - | ProphetpayPaymentStatus::DuplicateValue - | ProphetpayPaymentStatus::MissingProfile - | ProphetpayPaymentStatus::EmptyRef => Self::Failure, - ProphetpayPaymentStatus::Voided => Self::Voided, +#[serde(rename_all = "camelCase")] +pub struct ProphetpayCompleteAuthResponse { + pub success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, + pub response_code: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProphetpayCardTokenData { + card_token: Secret, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + ProphetpayCompleteAuthResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + if item.response.success { + let card_token = get_card_token(item.data.request.redirect_response.clone())?; + let card_token_data = ProphetpayCardTokenData { + card_token: Secret::from(card_token), + }; + let connector_metadata = serde_json::to_value(card_token_data).ok(); + Ok(Self { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) } } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ProphetpayResponse { - pub response_text: ProphetpayPaymentStatus, +pub struct ProphetpaySyncResponse { + success: bool, + pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, + pub response_code: String, } -impl TryFrom> +impl + TryFrom> for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData, ) -> Result { - Ok(Self { - status: enums::AttemptStatus::from(item.response.response_text), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction_id, - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - }), - ..item.data - }) + if item.response.success { + Ok(Self { + status: enums::AttemptStatus::Charged, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayVoidResponse { + pub success: bool, + pub response_text: String, + #[serde(rename = "transactionID")] + pub transaction_id: String, + pub response_code: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + status: enums::AttemptStatus::Voided, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::VoidFailed, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } } } @@ -435,8 +540,8 @@ impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { let transaction_id = item.request.connector_transaction_id.to_owned(); Ok(Self { transaction_id, - ref_info: item.attempt_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.attempt_id), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) @@ -447,6 +552,7 @@ impl TryFrom<&types::PaymentsCancelRouterData> for ProphetpayVoidRequest { #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundRequest { pub amount: f64, + pub card_token: Secret, pub transaction_id: String, pub profile: Secret, pub ref_info: String, @@ -459,47 +565,26 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet fn try_from( item: &ProphetpayRouterData<&types::RefundsRouterData>, ) -> Result { - let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; - let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); - Ok(Self { - transaction_id, - amount: item.amount.to_owned(), - profile: auth_data.profile_id, - ref_info: item.router_data.request.refund_id.to_owned(), - inquiry_reference: format!("inquiry_{}", item.router_data.request.refund_id), - action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), - }) - } -} - -#[allow(dead_code)] -#[derive(Debug, Deserialize, Clone)] -pub enum RefundStatus { - Success, - Failure, - #[serde(rename = "Transaction Voided")] - Voided, - #[serde(rename = "Requires a card on file.")] - CardTokenNotFound, - #[serde(rename = "RefInfo and InquiryReference are duplicated")] - DuplicateValue, - #[serde(rename = "Profile is missing")] - MissingProfile, - #[serde(rename = "RefInfo is empty.")] - EmptyRef, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Success - // in retrieving refund, if it is successful, it is shown as voided - | RefundStatus::Voided => Self::Success, - RefundStatus::Failure - | RefundStatus::CardTokenNotFound - | RefundStatus::DuplicateValue - | RefundStatus::MissingProfile - | RefundStatus::EmptyRef => Self::Failure, + if item.router_data.request.payment_amount == item.router_data.request.refund_amount { + let auth_data = ProphetpayAuthType::try_from(&item.router_data.connector_auth_type)?; + let transaction_id = item.router_data.request.connector_transaction_id.to_owned(); + let card_token_data: ProphetpayCardTokenData = + to_connector_meta(item.router_data.request.connector_metadata.clone())?; + + Ok(Self { + transaction_id, + amount: item.amount.to_owned(), + card_token: card_token_data.card_token, + profile: auth_data.profile_id, + ref_info: item.router_data.connector_request_reference_id.to_owned(), + inquiry_reference: item.router_data.connector_request_reference_id.clone(), + action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), + }) + } else { + Err(errors::ConnectorError::NotImplemented( + "Partial Refund is Not Supported".to_string(), + ) + .into()) } } } @@ -507,7 +592,10 @@ impl From for enums::RefundStatus { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundResponse { - pub response_text: RefundStatus, + pub success: bool, + pub response_text: String, + pub tran_seq_number: String, + pub response_code: String, } impl TryFrom> @@ -517,20 +605,75 @@ impl TryFrom, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - // no refund id is generated, rather transaction id is used for referring to status in refund also - connector_refund_id: item.data.request.connector_transaction_id.clone(), - refund_status: enums::RefundStatus::from(item.response.response_text), - }), - ..item.data - }) + if item.response.success { + Ok(Self { + response: Ok(types::RefundsResponseData { + // no refund id is generated, tranSeqNumber is kept for future usage + connector_refund_id: item.response.tran_seq_number, + refund_status: enums::RefundStatus::Success, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } } } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProphetpayRefundSyncResponse { + pub success: bool, + pub response_text: String, + pub response_code: String, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + if item.response.success { + Ok(Self { + response: Ok(types::RefundsResponseData { + // no refund id is generated, rather transaction id is used for referring to status in refund also + connector_refund_id: item.data.request.connector_transaction_id.clone(), + refund_status: enums::RefundStatus::Success, + }), + ..item.data + }) + } else { + Ok(Self { + status: enums::AttemptStatus::Failure, + response: Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.response_text.clone(), + reason: Some(item.response.response_text), + status_code: item.http_code, + attempt_status: None, + }), + ..item.data + }) + } + } +} #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProphetpayRefundSyncRequest { + transaction_id: String, + inquiry_reference: String, ref_info: String, profile: Secret, action_type: i8, @@ -541,36 +684,11 @@ impl TryFrom<&types::RefundSyncRouterData> for ProphetpayRefundSyncRequest { fn try_from(item: &types::RefundSyncRouterData) -> Result { let auth_data = ProphetpayAuthType::try_from(&item.connector_auth_type)?; Ok(Self { - ref_info: item.attempt_id.to_owned(), + transaction_id: item.request.connector_transaction_id.clone(), + ref_info: item.connector_request_reference_id.to_owned(), + inquiry_reference: item.connector_request_reference_id.clone(), profile: auth_data.profile_id, action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Inquiry), }) } } - -impl TryFrom> - for types::RefundsRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.data.request.connector_transaction_id.clone(), - refund_status: enums::RefundStatus::from(item.response.response_text), - }), - ..item.data - }) - } -} - -// Error Response body is yet to be confirmed with the connector -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ProphetpayErrorResponse { - pub status: u16, - pub title: String, - pub trace_id: String, - pub errors: serde_json::Value, -} From 245e489d13209da19d6e9af01219056eec04e897 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:21:39 +0000 Subject: [PATCH 032/443] test(postman): update postman collection files --- postman/collection-json/stripe.postman_collection.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 06ccae91b2c7..9c9a8a5d685c 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -8504,7 +8504,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8784,7 +8784,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -9436,7 +9436,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -13998,7 +13998,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", From fcd206b6af0e0afdb8276077c61adc53f030e471 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:21:40 +0000 Subject: [PATCH 033/443] chore(version): v1.86.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe558180021..7d7b6770d471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.86.0 (2023-11-21) + +### Features + +- **connector:** [Prophetpay] Save card token for Refund and remove Void flow ([#2927](https://github.com/juspay/hyperswitch/pull/2927)) ([`15a255e`](https://github.com/juspay/hyperswitch/commit/15a255ea60dffad9e4cf20d642636028c27c7c00)) +- Add support for 3ds and surcharge decision through routing rules ([#2869](https://github.com/juspay/hyperswitch/pull/2869)) ([`f8618e0`](https://github.com/juspay/hyperswitch/commit/f8618e077065d94aa27d7153fc5ea6f93870bd81)) + +### Bug Fixes + +- **mca:** Change the check for `disabled` field in mca create and update ([#2938](https://github.com/juspay/hyperswitch/pull/2938)) ([`e66ccde`](https://github.com/juspay/hyperswitch/commit/e66ccde4cf6d055b7d02c5e982d2e09364845602)) +- Status goes from pending to partially captured in psync ([#2915](https://github.com/juspay/hyperswitch/pull/2915)) ([`3f3b797`](https://github.com/juspay/hyperswitch/commit/3f3b797dc65c1bc6f710b122ef00d5bcb409e600)) + +### Testing + +- **postman:** Update postman collection files ([`245e489`](https://github.com/juspay/hyperswitch/commit/245e489d13209da19d6e9af01219056eec04e897)) + +**Full Changelog:** [`v1.85.0...v1.86.0`](https://github.com/juspay/hyperswitch/compare/v1.85.0...v1.86.0) + +- - - + + ## 1.85.0 (2023-11-21) ### Features From f8261a96e758498a32c988191bf314aa6c752059 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Tue, 21 Nov 2023 22:38:40 +0530 Subject: [PATCH 034/443] feat(router): migrate `payment_method_data` to rust locker only if `payment_method` is card (#2929) --- crates/router/src/core/locker_migration.rs | 61 ++++++++++++------- .../router/src/core/payment_methods/cards.rs | 15 +++-- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index f036a03a2f0e..3f56cddee126 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -1,6 +1,6 @@ use api_models::{enums as api_enums, locker_migration::MigrateCardResponse}; use common_utils::errors::CustomResult; -use diesel_models::PaymentMethod; +use diesel_models::{enums as storage_enums, PaymentMethod}; use error_stack::{FutureExt, ResultExt}; use futures::TryFutureExt; @@ -79,10 +79,21 @@ pub async fn call_to_locker( ) -> CustomResult { let mut cards_moved = 0; - for pm in payment_methods { + for pm in payment_methods + .into_iter() + .filter(|pm| matches!(pm.payment_method, storage_enums::PaymentMethod::Card)) + { let card = cards::get_card_from_locker(state, customer_id, merchant_id, &pm.payment_method_id) - .await?; + .await; + + let card = match card { + Ok(card) => card, + Err(err) => { + logger::error!("Failed to fetch card from Basilisk HS locker : {:?}", err); + continue; + } + }; let card_details = api::CardDetail { card_number: card.card_number, @@ -103,28 +114,36 @@ pub async fn call_to_locker( card_network: card.card_brand, }; - let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs( - state, - pm_create, - &card_details, - customer_id.to_string(), - merchant_account, - api_enums::LockerChoice::Tartarus, - Some(&pm.payment_method_id), - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable(format!( - "Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", - pm.payment_method_id - ))?; + let add_card_result = cards::add_card_hs( + state, + pm_create, + &card_details, + customer_id.to_string(), + merchant_account, + api_enums::LockerChoice::Tartarus, + Some(&pm.payment_method_id), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + )); + + let (_add_card_rs_resp, _is_duplicate) = match add_card_result { + Ok(output) => output, + Err(err) => { + logger::error!("Failed to add card to Rust locker : {:?}", err); + continue; + } + }; cards_moved += 1; logger::info!( - "Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", - pm.payment_method_id - ); + "Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ", + pm.payment_method_id + ); } Ok(cards_moved) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 9736edc73987..ad42a8579127 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -254,11 +254,18 @@ pub async fn add_card_to_locker( &metrics::CARD_ADD_TIME, &[], ) - .await?; - - logger::debug!("card added to rust locker"); + .await; - Ok(add_card_to_rs_resp) + match add_card_to_rs_resp { + value @ Ok(_) => { + logger::debug!("Card added successfully"); + value + } + Err(err) => { + logger::debug!(error =? err,"failed to add card"); + Ok(add_card_to_hs_resp) + } + } } pub async fn get_card_from_locker( From a701db70caff63517dde42d1d094a6b3dc39ef26 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:00:38 +0530 Subject: [PATCH 035/443] CI: update release new version workflow to not generate release notes (#2941) --- .github/git-cliff-release.toml | 89 ----------------------- .github/workflows/release-new-version.yml | 27 +------ 2 files changed, 1 insertion(+), 115 deletions(-) delete mode 100644 .github/git-cliff-release.toml diff --git a/.github/git-cliff-release.toml b/.github/git-cliff-release.toml deleted file mode 100644 index 1b82c812b5d8..000000000000 --- a/.github/git-cliff-release.toml +++ /dev/null @@ -1,89 +0,0 @@ -# configuration file for git-cliff -# see https://github.com/orhun/git-cliff#configuration-file - -[changelog] -# changelog header -header = "" -# template for the changelog body -# https://tera.netlify.app/docs/#introduction -body = """ -{% set newline = "\n" -%} -{% set commit_base_url = "https://github.com/juspay/hyperswitch/commit/" -%} -{% set compare_base_url = "https://github.com/juspay/hyperswitch/compare/" -%} -{% if version -%} - ## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }}) -{% else -%} - ## [unreleased] -{% endif -%} -{% for group, commits in commits | group_by(attribute="group") %} - {# The `striptags` removes the HTML comments added while grouping -#} - ### {{ group | striptags | trim | upper_first }} - {% for scope, commits in commits | group_by(attribute="scope") %} - - {{ "**" ~ scope ~ ":" ~ "**" -}} - {% for commit in commits -%} - {% if commits | length != 1 %}{{ newline ~ " - " }}{% else %}{{ " " }}{% endif -%} - {{ commit.message | upper_first | trim }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ commit_base_url ~ commit.id }})) by {{ commit.author.email -}} - {%- endfor -%} - {%- endfor -%} - {%- for commit in commits -%} - {% if commit.scope %}{% else %} - - {{ commit.message | upper_first | trim }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ commit_base_url ~ commit.id }})) by {{ commit.author.email -}} - {%- endif %} - {%- endfor %} -{% endfor %} -{% if previous and previous.commit_id and commit_id -%} - **Full Changelog:** [`{{ previous.version }}...{{ version }}`]({{ compare_base_url }}{{ previous.version }}...{{ version }})\n -{% endif %} -""" -# remove the leading and trailing whitespace from the template -trim = true -# changelog footer -footer = "" - -[git] -# parse the commits based on https://www.conventionalcommits.org -conventional_commits = true -# filter out the commits that are not conventional -filter_unconventional = false -# process each line of a commit as an individual commit -split_commits = false -# regex for preprocessing the commit messages -commit_preprocessors = [ - { pattern = "^ +", replace = "" }, # remove spaces at the beginning of the message - { pattern = " +", replace = " " }, # replace multiple spaces with a single space - { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/juspay/hyperswitch/pull/${1}))" }, # replace PR numbers with links - { pattern = "(\\n?Co-authored-by: .+ <.+@.+>\\n?)+", replace = "" }, # remove co-author information - { pattern = "(\\n?Signed-off-by: .+ <.+@.+>\\n?)+", replace = "" }, # remove sign-off information -] -# regex for parsing and grouping commits -# the HTML comments (``) are a workaround to get sections in custom order, since `git-cliff` sorts sections in alphabetical order -# reference: https://github.com/orhun/git-cliff/issues/9 -commit_parsers = [ - { message = "^(?i)(feat)", group = "Features" }, - { message = "^(?i)(fix)", group = "Bug Fixes" }, - { message = "^(?i)(perf)", group = "Performance" }, - { body = ".*security", group = "Security" }, - { message = "^(?i)(refactor)", group = "Refactors" }, - { message = "^(?i)(test)", group = "Testing" }, - { message = "^(?i)(docs)", group = "Documentation" }, - { message = "^(?i)(chore\\(version\\)): V[\\d]+\\.[\\d]+\\.[\\d]+", skip = true }, - { message = "^(?i)(chore)", group = "Miscellaneous Tasks" }, - { message = "^(?i)(build)", group = "Build System / Dependencies" }, - { message = "^(?i)(ci)", skip = true }, -] -# protect breaking changes from being skipped due to matching a skipping commit_parser -protect_breaking_commits = false -# filter out the commits that are not matched by commit parsers -filter_commits = false -# glob pattern for matching git tags -tag_pattern = "v[0-9]*" -# regex for skipping tags -# skip_tags = "v0.1.0-beta.1" -# regex for ignoring tags -# ignore_tags = "" -# sort the tags topologically -topo_order = true -# sort the commits inside sections by oldest/newest order -sort_commits = "oldest" -# limit the number of commits included in the changelog. -# limit_commits = 42 diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index 872c207e8aa3..eda2df05153b 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -40,19 +40,6 @@ jobs: crate: cocogitto version: 5.4.0 - - name: Install git-cliff - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: git-cliff - version: 1.2.0 - - - name: Install changelog-gh-usernames - uses: baptiste0928/cargo-install@v2.1.0 - with: - crate: changelog-gh-usernames - git: https://github.com/SanchithHegde/changelog-gh-usernames - rev: dab6da3ff99dbbff8650c114984c4d8be5161ac8 - - name: Set Git Configuration shell: bash run: | @@ -87,7 +74,7 @@ jobs: PREVIOUS_TAG="$(git tag --sort='version:refname' --merged | tail --lines 1)" if [[ "$(cog bump --auto --dry-run)" == *"No conventional commits for your repository that required a bump"* ]]; then NEW_TAG="$(cog bump --patch --dry-run)" - elif [[ "${PREVIOUS_TAG}" != "${NEW_TAG}" ]]; then + else NEW_TAG="$(cog bump --auto --dry-run)" fi echo "NEW_TAG=${NEW_TAG}" >> $GITHUB_ENV @@ -106,15 +93,3 @@ jobs: run: | git push git push --tags - - - name: Generate release notes and create GitHub release - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - env: - GITHUB_TOKEN: ${{ github.token }} - GH_TOKEN: ${{ secrets.AUTO_RELEASE_PAT }} - # Need to consider commits inclusive of previous tag to generate diff link between versions. - # This would also then require us to remove the last few lines from the changelog. - run: | - git-cliff --config .github/git-cliff-release.toml "${PREVIOUS_TAG}^..${NEW_TAG}" | changelog-gh-usernames | sed "/## ${PREVIOUS_TAG#v}/,\$d" > release-notes.md - gh release create "${NEW_TAG}" --notes-file release-notes.md --verify-tag --title "Hyperswitch ${NEW_TAG}" From 7f74ae98a1d48eed98341e4505d3801a61e69fc7 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:35:40 +0530 Subject: [PATCH 036/443] fix: cybersource mandates and fiserv exp year (#2920) Co-authored-by: Arjun Karthik --- crates/router/src/connector/cybersource.rs | 94 ++++- .../src/connector/cybersource/transformers.rs | 388 +++++++++++++----- .../src/connector/fiserv/transformers.rs | 37 +- crates/router/src/connector/utils.rs | 10 + crates/router/src/core/payments.rs | 24 +- 5 files changed, 427 insertions(+), 126 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index f69701f73958..ce283b12b798 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -94,7 +94,7 @@ impl ConnectorCommon for Cybersource { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Minor + api::CurrencyUnit::Base } fn build_error_response( @@ -252,6 +252,80 @@ impl types::PaymentsResponseData, > for Cybersource { + fn get_headers( + &self, + req: &types::SetupMandateRouterData, + 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::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = cybersource::CybersourceZeroMandateRequest::try_from(req)?; + let cybersource_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(cybersource_req)) + } + + fn build_request( + &self, + req: &types::SetupMandateRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::SetupMandateRouterData, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourcePaymentsResponse = res + .response + .parse_struct("CybersourceMandateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + false, + )) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -300,7 +374,14 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourcePaymentsRequest::try_from(req)?; + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount_to_capture, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsCaptureRequest::try_from(&connector_router_data)?; let cybersource_payments_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, @@ -665,7 +746,14 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = cybersource::CybersourceRefundRequest::try_from(req)?; + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let connector_request = + cybersource::CybersourceRefundRequest::try_from(&connector_router_data)?; let cybersource_refund_request = types::RequestBody::log_and_get_request_body( &connector_request, utils::Encode::::encode_to_string_of_json, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 9233a95d7dd7..0e81b6b59dff 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -4,10 +4,12 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, AddressDetailsData, PhoneDetailsData, RouterData}, + connector::utils::{ + self, AddressDetailsData, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, + PhoneDetailsData, RouterData, + }, consts, core::errors, - pii::PeekInterface, types::{ self, api::{self, enums as api_enums}, @@ -46,7 +48,81 @@ impl } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceZeroMandateRequest { + processing_information: ProcessingInformation, + payment_information: PaymentInformation, + order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::SetupMandateRouterData) -> Result { + let phone = item.get_billing_phone()?; + let number_with_code = phone.get_number_with_country_code()?; + let email = item.request.get_email()?; + let bill_to = build_bill_to(item.get_billing()?, email, number_with_code)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: "0".to_string(), + currency: item.request.currency.to_string(), + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: CybersourcePaymentInitiatorTypes::Customer, + credential_stored_on_file: true, + }, + }), + ); + + let processing_information = ProcessingInformation { + capture: Some(false), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.connector_request_reference_id.clone()), + }; + + let payment_information = match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(ccard) => { + let card = CardDetails::PaymentCard(Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + }); + PaymentInformation { + card, + instrument_identifier: None, + } + } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))?, + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsRequest { processing_information: ProcessingInformation, @@ -55,26 +131,82 @@ pub struct CybersourcePaymentsRequest { client_reference_information: ClientReferenceInformation, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { - capture: bool, + action_list: Option>, + action_token_types: Option>, + authorization_options: Option, + commerce_indicator: CybersourceCommerceIndicator, + capture: Option, capture_options: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceActionsList { + TokenCreate, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceActionsTokenType { + InstrumentIdentifier, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentInitiator { + #[serde(rename = "type")] + initiator_type: CybersourcePaymentInitiatorTypes, + credential_stored_on_file: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourcePaymentInitiatorTypes { + Customer, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CybersourceCommerceIndicator { + Internet, +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { capture_sequence_number: u32, total_capture_count: u32, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct PaymentInformation { - card: Card, + card: CardDetails, + instrument_identifier: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CybersoucreInstrumentIdentifier { + id: String, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum CardDetails { + PaymentCard(Card), + MandateCard(MandateCardDetails), } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { number: cards::CardNumber, @@ -83,27 +215,34 @@ pub struct Card { security_code: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MandateCardDetails { + expiration_month: Secret, + expiration_year: Secret, +} + +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { amount_details: Amount, - bill_to: BillTo, + bill_to: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformation { amount_details: Amount, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Amount { total_amount: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { first_name: Secret, @@ -147,104 +286,135 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { + let phone = item.router_data.get_billing_phone()?; + let number_with_code = phone.get_number_with_country_code()?; + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; + + let order_information = OrderInformationWithBill { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency.to_string(), + }, + bill_to: Some(bill_to), + }; + let (action_list, action_token_types, authorization_options) = + if item.router_data.request.setup_future_usage.is_some() { + ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: CybersourcePaymentInitiatorTypes::Customer, + credential_stored_on_file: true, + }, + }), + ) + } else { + (None, None, None) + }; + + let processing_information = ProcessingInformation { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + }; + + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_information = match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(ccard) => { - let phone = item.router_data.get_billing_phone()?; - let phone_number = phone.get_number()?; - let country_code = phone.get_country_code()?; - let number_with_code = - Secret::new(format!("{}{}", country_code, phone_number.peek())); - let email = item - .router_data - .request - .email - .clone() - .ok_or_else(utils::missing_field_err("email"))?; - let bill_to = - build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; - - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency.to_string().to_uppercase(), - }, - bill_to, - }; - - let payment_information = PaymentInformation { - card: Card { + let instrument_identifier = + item.router_data + .request + .connector_mandate_id() + .map(|mandate_token_id| CybersoucreInstrumentIdentifier { + id: mandate_token_id, + }); + let card = if instrument_identifier.is_some() { + CardDetails::MandateCard(MandateCardDetails { + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + }) + } else { + CardDetails::PaymentCard(Card { number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, security_code: ccard.card_cvc, - }, - }; - - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - capture_options: None, - }; - - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), + }) }; - - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) + PaymentInformation { + card, + instrument_identifier, + } } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), - } + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))? + } + }; + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) } } -impl TryFrom<&types::PaymentsCaptureRouterData> for CybersourcePaymentsRequest { +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsCaptureRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationWithBill, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> + for CybersourcePaymentsCaptureRequest +{ type Error = error_stack::Report; - fn try_from(value: &types::PaymentsCaptureRouterData) -> Result { + fn try_from( + item: &CybersourceRouterData<&types::PaymentsCaptureRouterData>, + ) -> Result { Ok(Self { processing_information: ProcessingInformation { capture_options: Some(CaptureOptions { capture_sequence_number: 1, total_capture_count: 1, }), - ..Default::default() + action_list: None, + action_token_types: None, + authorization_options: None, + capture: None, + commerce_indicator: CybersourceCommerceIndicator::Internet, }, order_information: OrderInformationWithBill { amount_details: Amount { - total_amount: value.request.amount_to_capture.to_string(), - ..Default::default() + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, - ..Default::default() - }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), + bill_to: None, }, - ..Default::default() - }) - } -} - -impl TryFrom<&types::RefundExecuteRouterData> for CybersourcePaymentsRequest { - type Error = error_stack::Report; - fn try_from(value: &types::RefundExecuteRouterData) -> Result { - Ok(Self { - order_information: OrderInformationWithBill { - amount_details: Amount { - total_amount: value.request.refund_amount.to_string(), - currency: value.request.currency.to_string(), - }, - ..Default::default() - }, - client_reference_information: ClientReferenceInformation { - code: Some(value.connector_request_reference_id.clone()), - }, - ..Default::default() }) } } @@ -274,7 +444,7 @@ impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { } } } -#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CybersourcePaymentStatus { Authorized, @@ -318,22 +488,29 @@ impl From for enums::RefundStatus { } } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsResponse { id: String, status: CybersourcePaymentStatus, error_information: Option, client_reference_information: Option, + token_information: Option, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceTokenInformation { + instrument_identifier: CybersoucreInstrumentIdentifier, +} + +#[derive(Debug, Clone, Deserialize)] pub struct CybersourceErrorInformation { reason: String, message: String, @@ -359,6 +536,13 @@ impl ) -> Result { let item = data.0; let is_capture = data.1; + let mandate_reference = + item.response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); Ok(Self { status: get_payment_status(is_capture, item.response.status.into()), response: match item.response.error_information { @@ -374,7 +558,7 @@ impl item.response.id.clone(), ), redirection_data: None, - mandate_reference: None, + mandate_reference, connector_metadata: None, network_txn_id: None, connector_response_reference_id: item @@ -495,26 +679,28 @@ pub struct Details { pub reason: String, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Deserialize)] pub struct ErrorInformation { pub message: String, pub reason: String, } -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { order_information: OrderInformation, } -impl TryFrom<&types::RefundsRouterData> for CybersourceRefundRequest { +impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for CybersourceRefundRequest { type Error = error_stack::Report; - fn try_from(item: &types::RefundsRouterData) -> Result { + fn try_from( + item: &CybersourceRouterData<&types::RefundsRouterData>, + ) -> Result { Ok(Self { order_information: OrderInformation { amount_details: Amount { - total_amount: item.request.refund_amount.to_string(), - currency: item.request.currency.to_string(), + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), }, }, }) diff --git a/crates/router/src/connector/fiserv/transformers.rs b/crates/router/src/connector/fiserv/transformers.rs index f8d88d08c6ba..5add9b79e3f9 100644 --- a/crates/router/src/connector/fiserv/transformers.rs +++ b/crates/router/src/connector/fiserv/transformers.rs @@ -3,7 +3,10 @@ use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsCancelRequestData, PaymentsSyncRequestData, RouterData}, + connector::utils::{ + self, CardData as CardDataUtil, PaymentsCancelRequestData, PaymentsSyncRequestData, + RouterData, + }, core::errors, pii::Secret, types::{self, api, storage::enums}, @@ -41,7 +44,7 @@ impl } } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservPaymentsRequest { amount: Amount, @@ -51,7 +54,7 @@ pub struct FiservPaymentsRequest { transaction_interaction: TransactionInteraction, } -#[derive(Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(tag = "sourceType")] pub enum Source { PaymentCard { @@ -65,7 +68,7 @@ pub enum Source { }, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CardData { card_data: cards::CardNumber, @@ -74,7 +77,7 @@ pub struct CardData { security_code: Secret, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct GooglePayToken { signature: String, @@ -82,14 +85,14 @@ pub struct GooglePayToken { protocol_version: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] pub struct Amount { #[serde(serialize_with = "utils::str_to_f32")] total: String, currency: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TransactionDetails { capture_flag: Option, @@ -97,14 +100,14 @@ pub struct TransactionDetails { merchant_transaction_id: String, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MerchantDetails { merchant_id: Secret, terminal_id: Option, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TransactionInteraction { origin: TransactionInteractionOrigin, @@ -112,19 +115,19 @@ pub struct TransactionInteraction { pos_condition_code: TransactionInteractionPosConditionCode, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum TransactionInteractionOrigin { #[default] Ecom, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TransactionInteractionEciIndicator { #[default] ChannelEncrypted, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TransactionInteractionPosConditionCode { #[default] @@ -174,7 +177,7 @@ impl TryFrom<&FiservRouterData<&types::PaymentsAuthorizeRouterData>> for FiservP let card = CardData { card_data: ccard.card_number.clone(), expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.card_exp_year.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), security_code: ccard.card_cvc.clone(), }; Source::PaymentCard { card } @@ -219,7 +222,7 @@ impl TryFrom<&types::ConnectorAuthType> for FiservAuthType { } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservCancelRequest { transaction_details: TransactionDetails, @@ -406,7 +409,7 @@ impl TryFrom> for FiservCap } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FiservSyncRequest { merchant_details: MerchantDetails, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8b20332ce5ed..a098cef5b778 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -322,6 +322,7 @@ impl PaymentsCaptureRequestData for types::PaymentsCaptureData { pub trait PaymentsSetupMandateRequestData { fn get_browser_info(&self) -> Result; + fn get_email(&self) -> Result; } impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { @@ -330,6 +331,9 @@ impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { .clone() .ok_or_else(missing_field_err("browser_info")) } + fn get_email(&self) -> Result { + self.email.clone().ok_or_else(missing_field_err("email")) + } } pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> Result; @@ -869,6 +873,7 @@ impl CryptoData for api::CryptoData { pub trait PhoneDetailsData { fn get_number(&self) -> Result, Error>; fn get_country_code(&self) -> Result; + fn get_number_with_country_code(&self) -> Result, Error>; } impl PhoneDetailsData for api::PhoneDetails { @@ -882,6 +887,11 @@ impl PhoneDetailsData for api::PhoneDetails { .clone() .ok_or_else(missing_field_err("billing.phone.number")) } + fn get_number_with_country_code(&self) -> Result, Error> { + let number = self.get_number()?; + let country_code = self.get_country_code()?; + Ok(Secret::new(format!("{}{}", country_code, number.peek()))) + } } pub trait AddressDetailsData { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8c13b05836f1..1c40ef81f497 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1663,10 +1663,24 @@ where .unwrap_or(false); let payment_data_and_tokenization_action = match connector { - Some(_) if is_mandate => ( - payment_data.to_owned(), - TokenizationAction::SkipConnectorTokenization, - ), + Some(connector_name) if is_mandate => { + if connector_name == *"cybersource" { + let (_operation, payment_method_data) = operation + .to_domain()? + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + ) + .await?; + payment_data.payment_method_data = payment_method_data; + } + ( + payment_data.to_owned(), + TokenizationAction::SkipConnectorTokenization, + ) + } Some(connector) if is_operation_confirm(&operation) => { let payment_method = &payment_data .payment_attempt @@ -1749,7 +1763,7 @@ where }; (payment_data.to_owned(), connector_tokenization_action) } - _ => ( + Some(_) | None => ( payment_data.to_owned(), TokenizationAction::SkipConnectorTokenization, ), From c6a5a8574825dc333602f4f1cee7e26969eab030 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 22 Nov 2023 01:13:01 +0530 Subject: [PATCH 037/443] chore: address Rust 1.74 clippy lints (#2942) --- crates/router/src/configs/defaults.rs | 4 ++-- crates/router/src/connector/powertranz/transformers.rs | 4 ++-- crates/router/src/core/payment_methods/cards.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index b71e2aad5b5d..a0da9c88ef35 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4356,8 +4356,8 @@ impl Default for super::settings::ApiKeys { #[cfg(feature = "kms")] kms_encrypted_hash_key: KmsValue::default(), - /// Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating - /// hashes of API keys + // Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating + // hashes of API keys #[cfg(not(feature = "kms"))] hash_key: String::new(), diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 83bca662ec21..5a8c49bd8ee1 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -150,8 +150,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for ExtendedData { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { Ok(Self { three_d_secure: ThreeDSecure { - /// Merchants preferred sized of challenge window presented to cardholder. - /// 5 maps to 100% of challenge window size + // Merchants preferred sized of challenge window presented to cardholder. + // 5 maps to 100% of challenge window size challenge_window_size: 5, }, merchant_response_url: item.request.get_complete_authorize_url()?, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index ad42a8579127..60fd3f315ea6 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -156,7 +156,7 @@ pub async fn add_payment_method( .await?; } - Ok(resp).map(services::ApplicationResponse::Json) + Ok(services::ApplicationResponse::Json(resp)) } #[instrument(skip_all)] From ce10579a729fe4a7d4ab9f1a4cbd38c3ca00e90b Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:55:51 +0530 Subject: [PATCH 038/443] feat(api_event_errors): error field in APIEvents (#2808) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> --- crates/api_models/src/errors/types.rs | 6 ++++-- crates/router/src/core/webhooks.rs | 1 + crates/router/src/events/api_logs.rs | 3 +++ crates/router/src/services/api.rs | 17 +++++++++++++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/api_models/src/errors/types.rs b/crates/api_models/src/errors/types.rs index 365be676f167..5f303f93c56b 100644 --- a/crates/api_models/src/errors/types.rs +++ b/crates/api_models/src/errors/types.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use reqwest::StatusCode; +use serde::Serialize; #[derive(Debug, serde::Serialize)] pub enum ErrorType { @@ -78,7 +79,8 @@ pub struct Extra { pub reason: Option, } -#[derive(Debug, Clone)] +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "type", content = "value")] pub enum ApiErrorResponse { Unauthorized(ApiError), ForbiddenCommonResource(ApiError), @@ -88,7 +90,7 @@ pub enum ApiErrorResponse { Unprocessable(ApiError), InternalServerError(ApiError), NotImplemented(ApiError), - ConnectorError(ApiError, StatusCode), + ConnectorError(ApiError, #[serde(skip_serializing)] StatusCode), NotFound(ApiError), MethodNotAllowed(ApiError), BadRequest(ApiError), diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 9bbe35ba2a9d..67154ae33aef 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -913,6 +913,7 @@ pub async fn webhooks_wrapper, url_path: String, response: Option, + error: Option, #[serde(flatten)] event_type: ApiEventsType, hs_latency: Option, @@ -52,6 +53,7 @@ impl ApiEvent { response: Option, hs_latency: Option, auth_type: AuthenticationType, + error: Option, event_type: ApiEventsType, http_req: &HttpRequest, ) -> Self { @@ -64,6 +66,7 @@ impl ApiEvent { request, response, auth_type, + error, ip_addr: http_req .connection_info() .realip_remote_addr() diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 0a8b84ffd11c..aae17195517d 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -769,7 +769,7 @@ where T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, E: ErrorSwitch + error_stack::Context, - OErr: ResponseError + error_stack::Context, + OErr: ResponseError + error_stack::Context + Serialize, errors::ApiErrorResponse: ErrorSwitch, { let request_id = RequestId::extract(request) @@ -826,7 +826,9 @@ where .as_millis(); let mut serialized_response = None; + let mut error = None; let mut overhead_latency = None; + let status_code = match output.as_ref() { Ok(res) => { if let ApplicationResponse::Json(data) = res { @@ -854,7 +856,17 @@ where metrics::request::track_response_status_code(res) } - Err(err) => err.current_context().status_code().as_u16().into(), + Err(err) => { + error.replace( + serde_json::to_value(err.current_context()) + .into_report() + .attach_printable("Failed to serialize json response") + .change_context(errors::ApiErrorResponse::InternalServerError.switch()) + .ok() + .into(), + ); + err.current_context().status_code().as_u16().into() + } }; let api_event = ApiEvent::new( @@ -866,6 +878,7 @@ where serialized_response, overhead_latency, auth_type, + error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, ); From 037e310aab5fac90ba33cdff2acda2f031261a6c Mon Sep 17 00:00:00 2001 From: Shanks Date: Wed, 22 Nov 2023 12:59:51 +0530 Subject: [PATCH 039/443] ci: update CODEOWNERS with hyperswitch-routing modules (#2933) --- .github/CODEOWNERS | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 638d5540d3d6..3024477bac20 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,6 +33,17 @@ crates/router/src/compatibility/ @juspay/hyperswitch-compatibility crates/router/src/core/ @juspay/hyperswitch-core +crates/api_models/src/routing.rs @juspay/hyperswitch-routing +crates/euclid @juspay/hyperswitch-routing +crates/euclid_macros @juspay/hyperswitch-routing +crates/euclid_wasm @juspay/hyperswitch-routing +crates/kgraph_utils @juspay/hyperswitch-routing +crates/router/src/routes/routing.rs @juspay/hyperswitch-routing +crates/router/src/core/routing @juspay/hyperswitch-routing +crates/router/src/core/routing.rs @juspay/hyperswitch-routing +crates/router/src/core/payments/routing @juspay/hyperswitch-routing +crates/router/src/core/payments/routing.rs @juspay/hyperswitch-routing + crates/router/src/scheduler/ @juspay/hyperswitch-process-tracker Dockerfile @juspay/hyperswitch-infra From b441a1f2f9d9d84601cf78a6e39145e8fb847593 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Wed, 22 Nov 2023 15:37:01 +0530 Subject: [PATCH 040/443] feat(router): add list payment link support (#2805) Co-authored-by: Sahkal Poddar Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- README.md | 1 - crates/api_models/src/events.rs | 1 + crates/api_models/src/payments.rs | 63 +++++++++++++++-- crates/common_utils/src/consts.rs | 3 + crates/diesel_models/src/payment_link.rs | 2 + crates/diesel_models/src/schema.rs | 2 + crates/router/src/core/payment_link.rs | 45 +++++++++++-- .../src/core/payment_link/payment_link.html | 7 +- .../payments/operations/payment_create.rs | 4 ++ crates/router/src/db/payment_link.rs | 32 ++++++++- crates/router/src/lib.rs | 2 +- crates/router/src/routes/app.rs | 10 ++- crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/payment_link.rs | 43 ++++++++++++ crates/router/src/types/api.rs | 5 +- crates/router/src/types/api/payment_link.rs | 29 ++++++++ .../router/src/types/storage/payment_link.rs | 67 ++++++++++++++++++- crates/router/src/types/transformers.rs | 12 ++-- crates/router_env/src/logger/types.rs | 2 + .../down.sql | 2 +- .../down.sql | 2 + .../up.sql | 2 + openapi/openapi_spec.json | 33 +++++---- 23 files changed, 330 insertions(+), 44 deletions(-) create mode 100644 crates/router/src/types/api/payment_link.rs create mode 100644 migrations/2023-11-06-065213_add_description_to_payment_link/down.sql create mode 100644 migrations/2023-11-06-065213_add_description_to_payment_link/up.sql diff --git a/README.md b/README.md index bc528da9bbf5..edc8cae5cf8e 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts    - 2. Sign-in to your AWS console. 3. Follow the instructions provided on the console to successfully deploy Hyperswitch diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 0ce7638b5ed1..782c02be7a3a 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -36,6 +36,7 @@ impl_misc_api_event_type!( MandateResponse, MandateRevokedResponse, RetrievePaymentLinkRequest, + PaymentLinkListConstraints, MandateId, DisputeListConstraints, RetrieveApiKeyResponse, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c427088d688d..508eeb8d7310 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3194,18 +3194,17 @@ pub struct PaymentLinkResponse { #[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct RetrievePaymentLinkResponse { pub payment_link_id: String, - pub payment_id: String, pub merchant_id: String, pub link_to_pay: String, pub amount: i64, - #[schema(value_type = Option, example = "USD")] - pub currency: Option, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: PrimitiveDateTime, - #[serde(with = "common_utils::custom_serde::iso8601")] - pub last_modified_at: PrimitiveDateTime, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub link_expiry: Option, + pub description: Option, + pub status: String, + #[schema(value_type = Option)] + pub currency: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3230,3 +3229,57 @@ pub struct PaymentLinkDetails { pub max_items_visible_after_collapse: i8, pub sdk_theme: Option, } + +#[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] +#[serde(deny_unknown_fields)] + +pub struct PaymentLinkListConstraints { + /// limit on the number of objects to return + pub limit: Option, + + /// The time at which payment link is created + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created: Option, + + /// Time less than the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.lt" + )] + pub created_lt: Option, + + /// Time greater than the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.gt" + )] + pub created_gt: Option, + + /// Time less than or equals to the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option", + rename = "created.lte" + )] + pub created_lte: Option, + + /// Time greater than or equals to the payment link created time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(rename = "created.gte")] + pub created_gte: Option, +} + +#[derive(Clone, Debug, serde::Serialize, ToSchema)] +pub struct PaymentLinkListResponse { + /// The number of payment links included in the list + pub size: usize, + // The list of payment link response objects + pub data: Vec, +} diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 60756192d66e..7f9533d7eadd 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -24,6 +24,9 @@ pub const PAYMENTS_LIST_MAX_LIMIT_V1: u32 = 100; /// Maximum limit for payments list post api with filters pub const PAYMENTS_LIST_MAX_LIMIT_V2: u32 = 20; +/// Maximum limit for payment link list get api +pub const PAYMENTS_LINK_LIST_LIMIT: u32 = 100; + /// surcharge percentage maximum precision length pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 264cc915b35a..999a6767d8f3 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -22,6 +22,7 @@ pub struct PaymentLink { pub fulfilment_time: Option, pub custom_merchant_name: Option, pub payment_link_config: Option, + pub description: Option, } #[derive( @@ -51,4 +52,5 @@ pub struct PaymentLinkNew { pub fulfilment_time: Option, pub custom_merchant_name: Option, pub payment_link_config: Option, + pub description: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 2ce4f2b6d9d4..33400635f052 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -678,6 +678,8 @@ diesel::table! { #[max_length = 64] custom_merchant_name -> Nullable, payment_link_config -> Nullable, + #[max_length = 255] + description -> Nullable, } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 89d345b28674..07fdf4ae4072 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -6,7 +6,9 @@ use common_utils::{ ext_traits::{OptionExt, ValueExt}, }; use error_stack::{IntoReport, ResultExt}; +use futures::future; use masking::{PeekInterface, Secret}; +use time::PrimitiveDateTime; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -14,7 +16,10 @@ use crate::{ errors::RouterResponse, routes::AppState, services, - types::{domain, storage::enums as storage_enums, transformers::ForeignFrom}, + types::{ + api::payment_link::PaymentLinkResponseExt, domain, storage::enums as storage_enums, + transformers::ForeignFrom, + }, }; pub async fn retrieve_payment_link( @@ -27,8 +32,12 @@ pub async fn retrieve_payment_link( .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let response = - api_models::payments::RetrievePaymentLinkResponse::foreign_from(payment_link_object); + let status = check_payment_link_status(payment_link_object.fulfilment_time); + + let response = api_models::payments::RetrievePaymentLinkResponse::foreign_from(( + payment_link_object, + status, + )); Ok(services::ApplicationResponse::Json(response)) } @@ -62,7 +71,7 @@ pub async fn intiate_payment_link_flow( storage_enums::IntentStatus::RequiresCapture, storage_enums::IntentStatus::RequiresMerchantAction, ], - "create payment link", + "use payment link for", )?; let payment_link = db @@ -197,6 +206,34 @@ fn validate_sdk_requirements( Ok((pub_key, currency, client_secret)) } +pub async fn list_payment_link( + state: AppState, + merchant: domain::MerchantAccount, + constraints: api_models::payments::PaymentLinkListConstraints, +) -> RouterResponse> { + let db = state.store.as_ref(); + let payment_link = db + .list_payment_link_by_merchant_id(&merchant.merchant_id, constraints) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to retrieve payment link")?; + let payment_link_list = future::try_join_all(payment_link.into_iter().map(|payment_link| { + api_models::payments::RetrievePaymentLinkResponse::from_db_payment_link(payment_link) + })) + .await?; + Ok(services::ApplicationResponse::Json(payment_link_list)) +} + +pub fn check_payment_link_status(fulfillment_time: Option) -> String { + let curr_time = Some(common_utils::date_time::now()); + + if curr_time > fulfillment_time { + "expired".to_string() + } else { + "active".to_string() + } +} + fn validate_order_details( order_details: Option>>, ) -> Result< diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index abacf0998f67..0ca4abd340d6 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -46,6 +46,7 @@ width: 100%; height: 100%; max-width: 1900px; + overflow: scroll; } #hyper-footer { @@ -418,6 +419,7 @@ margin-top: 20px; border-radius: 3px; border: 1px solid #e6e6e6; + width: 90vw; } .hyper-checkout-status-item { @@ -432,12 +434,15 @@ } .hyper-checkout-item-header { - width: 15ch; + min-width: 13ch; font-size: 12px; } .hyper-checkout-item-value { font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; } @keyframes loading { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 845915cc332c..eee937071d6b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -75,6 +75,7 @@ impl db, state, amount, + request.description.clone(), ) .await? } else { @@ -779,6 +780,7 @@ pub fn payments_create_request_validation( Ok((amount, currency)) } +#[allow(clippy::too_many_arguments)] async fn create_payment_link( request: &api::PaymentsRequest, payment_link_object: api_models::payments::PaymentLinkObject, @@ -787,6 +789,7 @@ async fn create_payment_link( db: &dyn StorageInterface, state: &AppState, amount: api::Amount, + description: Option, ) -> RouterResult> { let created_at @ last_modified_at = Some(common_utils::date_time::now()); let domain = if let Some(domain_name) = payment_link_object.merchant_custom_domain_name { @@ -817,6 +820,7 @@ async fn create_payment_link( created_at, last_modified_at, fulfilment_time: payment_link_object.link_expiry, + description, payment_link_config, custom_merchant_name: payment_link_object.custom_merchant_name, }; diff --git a/crates/router/src/db/payment_link.rs b/crates/router/src/db/payment_link.rs index 38b59b1d60de..5dc9871e707e 100644 --- a/crates/router/src/db/payment_link.rs +++ b/crates/router/src/db/payment_link.rs @@ -1,10 +1,11 @@ use error_stack::IntoReport; -use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, - types::storage, + db::MockDb, + services::Store, + types::storage::{self, PaymentLinkDbExt}, }; #[async_trait::async_trait] @@ -18,6 +19,12 @@ pub trait PaymentLinkInterface { &self, _payment_link: storage::PaymentLinkNew, ) -> CustomResult; + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -44,6 +51,18 @@ impl PaymentLinkInterface for Store { .map_err(Into::into) .into_report() } + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::PaymentLink::filter_by_constraints(&conn, merchant_id, payment_link_constraints) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -63,4 +82,13 @@ impl PaymentLinkInterface for MockDb { // TODO: Implement function for `MockDb`x Err(errors::StorageError::MockDbError)? } + + async fn list_payment_link_by_merchant_id( + &self, + _merchant_id: &str, + _payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + // TODO: Implement function for `MockDb`x + Err(errors::StorageError::MockDbError)? + } } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 58d77d9e02f4..0bc8e492c40c 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -133,7 +133,6 @@ pub fn mk_app( .service(routes::PaymentMethods::server(state.clone())) .service(routes::EphemeralKey::server(state.clone())) .service(routes::Webhooks::server(state.clone())) - .service(routes::PaymentLink::server(state.clone())); } #[cfg(feature = "olap")] @@ -147,6 +146,7 @@ pub fn mk_app( .service(routes::Routing::server(state.clone())) .service(routes::LockerMigrate::server(state.clone())) .service(routes::Gsm::server(state.clone())) + .service(routes::PaymentLink::server(state.clone())) .service(routes::User::server(state.clone())) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 79801e8e64f0..96bb47ea4e97 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,8 +19,11 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, user::*}; -use super::{cache::*, health::*, payment_link::*}; +use super::{ + admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, + user::*, +}; +use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] @@ -675,11 +678,12 @@ impl Cache { } pub struct PaymentLink; - +#[cfg(feature = "olap")] impl PaymentLink { pub fn server(state: AppState) -> Scope { web::scope("/payment_link") .app_data(web::Data::new(state)) + .service(web::resource("/list").route(web::post().to(payments_link_list))) .service( web::resource("/{payment_link_id}").route(web::get().to(payment_link_retrieve)), ) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 84b00867b98d..a9cf7b44a73d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -132,9 +132,12 @@ impl From for ApiIdentifier { | Flow::BusinessProfileDelete | Flow::BusinessProfileList => Self::Business, + Flow::PaymentLinkRetrieve | Flow::PaymentLinkInitiate | Flow::PaymentLinkList => { + Self::PaymentLink + } + Flow::Verification => Self::Verification, - Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, Flow::RustLockerMigration => Self::RustLockerMigration, Flow::GsmRuleCreate | Flow::GsmRuleRetrieve diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 7d6bf1a05f09..4c26ea71f7d5 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -80,3 +80,46 @@ pub async fn initiate_payment_link( )) .await } + +/// Payment Link - List +/// +/// To list the payment links +#[utoipa::path( + get, + path = "/payment_link/list", + params( + ("limit" = Option, Query, description = "The maximum number of payment_link Objects to include in the response"), + ("connector" = Option, Query, description = "The connector linked to payment_link"), + ("created_time" = Option, Query, description = "The time at which payment_link is created"), + ("created_time.lt" = Option, Query, description = "Time less than the payment_link created time"), + ("created_time.gt" = Option, Query, description = "Time greater than the payment_link created time"), + ("created_time.lte" = Option, Query, description = "Time less than or equals to the payment_link created time"), + ("created_time.gte" = Option, Query, description = "Time greater than or equals to the payment_link created time"), + ), + responses( + (status = 200, description = "The payment link list was retrieved successfully"), + (status = 401, description = "Unauthorized request") + ), + tag = "Payment Link", + operation_id = "List all Payment links", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentLinkList))] +pub async fn payments_link_list( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Query, +) -> impl Responder { + let flow = Flow::PaymentLinkList; + let payload = payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| list_payment_link(state, auth.merchant_account, payload), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index dc615c4e41fa..b7d2fc8db33e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -7,6 +7,7 @@ pub mod enums; pub mod ephemeral_key; pub mod files; pub mod mandates; +pub mod payment_link; pub mod payment_methods; pub mod payments; pub mod payouts; @@ -20,8 +21,8 @@ use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ - admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_methods::*, - payments::*, payouts::*, refunds::*, webhooks::*, + admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, + payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ diff --git a/crates/router/src/types/api/payment_link.rs b/crates/router/src/types/api/payment_link.rs new file mode 100644 index 000000000000..e56af6b4aec4 --- /dev/null +++ b/crates/router/src/types/api/payment_link.rs @@ -0,0 +1,29 @@ +pub use api_models::payments::RetrievePaymentLinkResponse; + +use crate::{ + core::{errors::RouterResult, payment_link}, + types::storage::{self}, +}; + +#[async_trait::async_trait] +pub(crate) trait PaymentLinkResponseExt: Sized { + async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult; +} + +#[async_trait::async_trait] +impl PaymentLinkResponseExt for RetrievePaymentLinkResponse { + async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult { + let status = payment_link::check_payment_link_status(payment_link.fulfilment_time); + Ok(Self { + link_to_pay: payment_link.link_to_pay, + payment_link_id: payment_link.payment_link_id, + amount: payment_link.amount, + description: payment_link.description, + created_at: payment_link.created_at, + merchant_id: payment_link.merchant_id, + link_expiry: payment_link.fulfilment_time, + currency: payment_link.currency, + status, + }) + } +} diff --git a/crates/router/src/types/storage/payment_link.rs b/crates/router/src/types/storage/payment_link.rs index 1fa2465e5131..4dd9e06b4b41 100644 --- a/crates/router/src/types/storage/payment_link.rs +++ b/crates/router/src/types/storage/payment_link.rs @@ -1 +1,66 @@ -pub use diesel_models::payment_link::{PaymentLink, PaymentLinkNew}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, ExpressionMethods, QueryDsl}; +pub use diesel_models::{ + payment_link::{PaymentLink, PaymentLinkNew}, + schema::payment_link::dsl, +}; +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + connection::PgPooledConn, + core::errors::{self, CustomResult}, + logger, +}; +#[async_trait::async_trait] + +pub trait PaymentLinkDbExt: Sized { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + payment_link_list_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::DatabaseError>; +} + +#[async_trait::async_trait] +impl PaymentLinkDbExt for PaymentLink { + async fn filter_by_constraints( + conn: &PgPooledConn, + merchant_id: &str, + payment_link_list_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::DatabaseError> { + let mut filter = ::table() + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .order(dsl::created_at.desc()) + .into_boxed(); + + if let Some(created_time) = payment_link_list_constraints.created { + filter = filter.filter(dsl::created_at.eq(created_time)); + } + if let Some(created_time_lt) = payment_link_list_constraints.created_lt { + filter = filter.filter(dsl::created_at.lt(created_time_lt)); + } + if let Some(created_time_gt) = payment_link_list_constraints.created_gt { + filter = filter.filter(dsl::created_at.gt(created_time_gt)); + } + if let Some(created_time_lte) = payment_link_list_constraints.created_lte { + filter = filter.filter(dsl::created_at.le(created_time_lte)); + } + if let Some(created_time_gte) = payment_link_list_constraints.created_gte { + filter = filter.filter(dsl::created_at.ge(created_time_gte)); + } + if let Some(limit) = payment_link_list_constraints.limit { + filter = filter.limit(limit); + } + + logger::debug!(query = %diesel::debug_query::(&filter).to_string()); + + filter + .get_results_async(conn) + .await + .into_report() + // The query built here returns an empty Vec when no records are found, and if any error does occur, + // it would be an internal database error, due to which we are raising a DatabaseError::Unknown error + .change_context(errors::DatabaseError::Others) + .attach_printable("Error filtering payment link by specified constraints") + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index b73ba0964fbf..45aad93371e2 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -992,18 +992,20 @@ impl } } -impl ForeignFrom for api_models::payments::RetrievePaymentLinkResponse { - fn foreign_from(payment_link_object: storage::PaymentLink) -> Self { +impl ForeignFrom<(storage::PaymentLink, String)> + for api_models::payments::RetrievePaymentLinkResponse +{ + fn foreign_from((payment_link_object, status): (storage::PaymentLink, String)) -> Self { Self { payment_link_id: payment_link_object.payment_link_id, - payment_id: payment_link_object.payment_id, merchant_id: payment_link_object.merchant_id, link_to_pay: payment_link_object.link_to_pay, amount: payment_link_object.amount, - currency: payment_link_object.currency, created_at: payment_link_object.created_at, - last_modified_at: payment_link_object.last_modified_at, link_expiry: payment_link_object.fulfilment_time, + description: payment_link_object.description, + currency: payment_link_object.currency, + status, } } } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index f6d61f550840..178f837fce18 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -223,6 +223,8 @@ pub enum Flow { PaymentLinkRetrieve, /// payment Link Initiate flow PaymentLinkInitiate, + /// Payment Link List flow + PaymentLinkList, /// Create a business profile BusinessProfileCreate, /// Update a business profile diff --git a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql index b5ffba726937..f16e2800598f 100644 --- a/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql +++ b/migrations/2023-10-31-070509_add_payment_link_config_in_payment_link_db/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; \ No newline at end of file +ALTER TABLE payment_link DROP COLUMN IF EXISTS payment_link_config; diff --git a/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql b/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql new file mode 100644 index 000000000000..b184a2ce3dd7 --- /dev/null +++ b/migrations/2023-11-06-065213_add_description_to_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_link DROP COLUMN IF EXISTS description; \ No newline at end of file diff --git a/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql b/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql new file mode 100644 index 000000000000..65a074063ed3 --- /dev/null +++ b/migrations/2023-11-06-065213_add_description_to_payment_link/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER table payment_link ADD COLUMN IF NOT EXISTS description VARCHAR (255); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9ca4dea4a1a8..df9df43a43ee 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11306,20 +11306,16 @@ "type": "object", "required": [ "payment_link_id", - "payment_id", "merchant_id", "link_to_pay", "amount", "created_at", - "last_modified_at" + "status" ], "properties": { "payment_link_id": { "type": "string" }, - "payment_id": { - "type": "string" - }, "merchant_id": { "type": "string" }, @@ -11330,26 +11326,29 @@ "type": "integer", "format": "int64" }, - "currency": { - "allOf": [ - { - "$ref": "#/components/schemas/Currency" - } - ], - "nullable": true - }, "created_at": { "type": "string", "format": "date-time" }, - "last_modified_at": { - "type": "string", - "format": "date-time" - }, "link_expiry": { "type": "string", "format": "date-time", "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "currency": { + "allOf": [ + { + "$ref": "#/components/schemas/Currency" + } + ], + "nullable": true } } }, From 7d223ee0d1b53c02421ed6bd1b5584362d7a7456 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:37:59 +0530 Subject: [PATCH 041/443] docs(README): Update feature support link (#2894) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index edc8cae5cf8e..8c5ad9e03b2d 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ You can find the latest list of payment processors, supported methods, and features [here][supported-connectors-and-features]. -[supported-connectors-and-features]: https://docs.google.com/spreadsheets/d/e/2PACX-1vQWHLza9m5iO4Ol-tEBx22_Nnq8Mb3ISCWI53nrinIGLK8eHYmHGnvXFXUXEut8AFyGyI9DipsYaBLG/pubhtml?gid=0&single=true +[supported-connectors-and-features]: https://hyperswitch.io/pm-list ### 🌟 Hosted Version From 46e13d54759168ad7667af08d5481ab510e5706a Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:46:33 +0530 Subject: [PATCH 042/443] refactor(macros): use syn2.0 (#2890) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- Cargo.lock | 48 +-- crates/common_enums/src/enums.rs | 45 ++- crates/diesel_models/src/enums.rs | 23 +- .../payments/operations/payment_approve.rs | 2 +- .../payments/operations/payment_cancel.rs | 2 +- .../payments/operations/payment_capture.rs | 2 +- .../operations/payment_complete_authorize.rs | 2 +- .../payments/operations/payment_confirm.rs | 2 +- .../payments/operations/payment_create.rs | 2 +- .../operations/payment_method_validate.rs | 2 +- .../payments/operations/payment_reject.rs | 2 +- .../payments/operations/payment_response.rs | 4 +- .../payments/operations/payment_session.rs | 2 +- .../core/payments/operations/payment_start.rs | 2 +- .../payments/operations/payment_status.rs | 2 +- .../payments/operations/payment_update.rs | 2 +- crates/router_derive/Cargo.toml | 6 +- crates/router_derive/src/lib.rs | 28 +- crates/router_derive/src/macros.rs | 5 +- .../src/macros/api_error/helpers.rs | 20 +- crates/router_derive/src/macros/diesel.rs | 180 ++++++--- .../src/macros/generate_schema.rs | 6 +- crates/router_derive/src/macros/helpers.rs | 13 +- crates/router_derive/src/macros/operation.rs | 343 +++++++++++------- .../router_derive/src/macros/try_get_enum.rs | 107 +++--- crates/storage_impl/src/redis/kv_store.rs | 2 +- 26 files changed, 495 insertions(+), 359 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 730b08774fa3..bf0ee2d110c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" dependencies = [ - "darling 0.20.3", + "darling", "parse-size", "proc-macro2", "quote", @@ -1858,38 +1858,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core 0.20.3", - "darling_macro 0.20.3", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -1906,24 +1882,13 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core 0.20.3", + "darling_core", "quote", "syn 2.0.38", ] @@ -4861,7 +4826,6 @@ dependencies = [ name = "router_derive" version = "0.1.0" dependencies = [ - "darling 0.14.4", "diesel", "indexmap 2.0.2", "proc-macro2", @@ -4869,7 +4833,7 @@ dependencies = [ "serde", "serde_json", "strum 0.24.1", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] @@ -5358,7 +5322,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" dependencies = [ - "darling 0.20.3", + "darling", "proc-macro2", "quote", "syn 2.0.38", diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index cf3c398f8f48..063e35933c43 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1,6 +1,5 @@ use std::num::{ParseFloatError, TryFromIntError}; -use router_derive; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[doc(hidden)] @@ -29,7 +28,7 @@ pub mod diesel_exports { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum AttemptStatus { @@ -107,7 +106,7 @@ impl AttemptStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum AuthenticationType { @@ -132,7 +131,7 @@ pub enum AuthenticationType { ToSchema, Hash, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CaptureStatus { @@ -163,7 +162,7 @@ pub enum CaptureStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum CaptureMethod { @@ -190,7 +189,7 @@ pub enum CaptureMethod { serde::Serialize, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ConnectorType { @@ -231,7 +230,7 @@ pub enum ConnectorType { strum::EnumVariantNames, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] pub enum Currency { AED, ALL, @@ -789,7 +788,7 @@ impl Currency { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventType { @@ -825,7 +824,7 @@ pub enum EventType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MerchantStorageScheme { @@ -848,7 +847,7 @@ pub enum MerchantStorageScheme { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum IntentStatus { @@ -882,7 +881,7 @@ pub enum IntentStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum FutureUsage { @@ -904,7 +903,7 @@ pub enum FutureUsage { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum PaymentMethodIssuerCode { @@ -1108,7 +1107,7 @@ pub enum PaymentMethod { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PaymentType { @@ -1132,7 +1131,7 @@ pub enum PaymentType { serde::Serialize, serde::Deserialize, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] pub enum RefundStatus { Failure, @@ -1157,7 +1156,7 @@ pub enum RefundStatus { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MandateStatus { @@ -1211,7 +1210,7 @@ pub enum CardNetwork { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DisputeStage { @@ -1235,7 +1234,7 @@ pub enum DisputeStage { strum::EnumString, ToSchema, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DisputeStatus { @@ -1265,7 +1264,7 @@ pub enum DisputeStatus { utoipa::ToSchema, Copy )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[rustfmt::skip] pub enum CountryAlpha2 { AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, @@ -1692,7 +1691,7 @@ pub enum CanadaStatesAbbreviation { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutStatus { @@ -1720,7 +1719,7 @@ pub enum PayoutStatus { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PayoutType { @@ -1775,7 +1774,7 @@ pub enum PayoutEntityType { ToSchema, Hash, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum PaymentSource { @@ -1842,7 +1841,7 @@ pub enum FrmSuggestion { utoipa::ToSchema, Copy, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum ReconStatus { @@ -1871,7 +1870,7 @@ pub enum ApplePayFlow { ToSchema, Default, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ConnectorStatus { diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 817fee633190..dc4a7614f587 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -21,6 +21,7 @@ pub mod diesel_exports { pub use common_enums::*; use common_utils::pii; use diesel::serialize::{Output, ToSql}; +use router_derive::diesel_enum; use time::PrimitiveDateTime; #[derive( @@ -34,7 +35,7 @@ use time::PrimitiveDateTime; strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithmKind { @@ -55,7 +56,7 @@ pub enum RoutingAlgorithmKind { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventClass { @@ -76,7 +77,7 @@ pub enum EventClass { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventObjectType { @@ -97,7 +98,7 @@ pub enum EventObjectType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum ProcessTrackerStatus { @@ -126,7 +127,7 @@ pub enum ProcessTrackerStatus { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum RefundType { @@ -149,7 +150,7 @@ pub enum RefundType { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MandateType { @@ -217,7 +218,7 @@ pub struct MandateAmountData { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum BankNames { @@ -348,7 +349,7 @@ pub enum BankNames { strum::Display, strum::EnumString, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckType { @@ -369,7 +370,7 @@ pub enum FraudCheckType { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "pg_enum")] +#[diesel_enum(storage_type = "db_enum")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckStatus { Fraud, @@ -393,7 +394,7 @@ pub enum FraudCheckStatus { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] pub enum FraudCheckLastStep { #[default] @@ -416,7 +417,7 @@ pub enum FraudCheckLastStep { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "text")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum UserStatus { diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index af52105c85d5..f51d7a93ee5e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -28,7 +28,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentApprove; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 096f900e7195..d4605b47c438 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -25,7 +25,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "cancel")] +#[operation(operations = "all", flow = "cancel")] pub struct PaymentCancel; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ef8e2b0153d4..5b89cfdbcf0b 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -24,7 +24,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "capture")] +#[operation(operations = "all", flow = "capture")] pub struct PaymentCapture; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 62759bd0fd9b..8b264edbb3d1 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -27,7 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct CompleteAuthorize; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index e85531050529..125787e1a30f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -34,7 +34,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentConfirm; #[async_trait] impl diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index eee937071d6b..ccf9fc3fad1c 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -34,7 +34,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentCreate; /// The `get_trackers` function for `PaymentsCreate` is an entrypoint for new payments diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 62f12cfbc90c..693fce236846 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -29,7 +29,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "verify")] +#[operation(operations = "all", flow = "verify")] pub struct PaymentMethodValidate; impl ValidateRequest diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index ae02dde4bc06..ae606187a0a1 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -24,7 +24,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(ops = "all", flow = "reject")] +#[operation(operations = "all", flow = "reject")] pub struct PaymentReject; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index d68215bec7a7..3734abfc6ab5 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -35,8 +35,8 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( - ops = "post_tracker", - flow = "syncdata,authorizedata,canceldata,capturedata,completeauthorizedata,approvedata,rejectdata,setupmandatedata,sessiondata" + operations = "post_update_tracker", + flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data" )] pub struct PaymentResponse; diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index cea6eb176672..6097a5e430ce 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -26,7 +26,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "session")] +#[operation(operations = "all", flow = "session")] pub struct PaymentSession; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 6d4281216b4f..3a4ae2c2e0de 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -25,7 +25,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "start")] +#[operation(operations = "all", flow = "start")] pub struct PaymentStart; #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index b31c406f0ecd..d0cd4b32d3c2 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -28,7 +28,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "sync")] +#[operation(operations = "all", flow = "sync")] pub struct PaymentStatus; impl Operation diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 817b14286809..75d3b6b82b4c 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -27,7 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(ops = "all", flow = "authorize")] +#[operation(operations = "all", flow = "authorize")] pub struct PaymentUpdate; #[async_trait] diff --git a/crates/router_derive/Cargo.toml b/crates/router_derive/Cargo.toml index b4e60a8c2a33..6f598e0f0502 100644 --- a/crates/router_derive/Cargo.toml +++ b/crates/router_derive/Cargo.toml @@ -12,14 +12,14 @@ proc-macro = true doctest = false [dependencies] -darling = "0.14.4" indexmap = "2.0.0" proc-macro2 = "1.0.56" quote = "1.0.26" -syn = { version = "1.0.109", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features +syn = { version = "2.0.5", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features +strum = { version = "0.24.1", features = ["derive"] } [dev-dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.24.1", features = ["derive"] } + diff --git a/crates/router_derive/src/lib.rs b/crates/router_derive/src/lib.rs index 3f34c156ae8f..109003e0cc41 100644 --- a/crates/router_derive/src/lib.rs +++ b/crates/router_derive/src/lib.rs @@ -2,6 +2,10 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] +use syn::parse_macro_input; + +use crate::macros::diesel::DieselEnumMeta; + mod macros; /// Uses the [`Debug`][Debug] implementation of a type to derive its [`Display`][Display] @@ -66,7 +70,7 @@ pub fn debug_as_display_derive(input: proc_macro::TokenStream) -> proc_macro::To /// Blue, /// } /// ``` -#[proc_macro_derive(DieselEnum)] +#[proc_macro_derive(DieselEnum, attributes(storage_type))] pub fn diesel_enum_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse_macro_input!(input as syn::DeriveInput); let tokens = @@ -104,16 +108,15 @@ pub fn diesel_enum_derive_string(input: proc_macro::TokenStream) -> proc_macro:: /// Derives the boilerplate code required for using an enum with `diesel` and a PostgreSQL database. /// -/// Storage Type can either be "text" or "pg_enum" -/// Choosing text will store the enum as text in the database, whereas pg_enum will map it to the -/// database enum +/// Storage Type can either be "text" or "db_enum" +/// Choosing text will store the enum as text in the database, whereas db_enum will map it to the +/// corresponding database enum /// -/// Works in tandem with the [`DieselEnum`][DieselEnum] and [`DieselEnumText`][DieselEnumText] derive macro to achieve the desired results. +/// Works in tandem with the [`DieselEnum`][DieselEnum] derive macro to achieve the desired results. /// The enum is required to implement (or derive) the [`ToString`][ToString] and the /// [`FromStr`][FromStr] traits for the [`DieselEnum`][DieselEnum] derive macro to be used. /// /// [DieselEnum]: crate::DieselEnum -/// [DieselEnumText]: crate::DieselEnumText /// [FromStr]: ::core::str::FromStr /// [ToString]: ::std::string::ToString /// @@ -138,12 +141,12 @@ pub fn diesel_enum( args: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let args = syn::parse_macro_input!(args as syn::AttributeArgs); + let args_parsed = parse_macro_input!(args as DieselEnumMeta); let item = syn::parse_macro_input!(item as syn::ItemEnum); - let tokens = macros::diesel_enum_attribute_inner(&args, &item) - .unwrap_or_else(|error| error.to_compile_error()); - tokens.into() + macros::diesel::diesel_enum_attribute_macro(args_parsed, &item) + .unwrap_or_else(|error| error.to_compile_error()) + .into() } /// A derive macro which generates the setter functions for any struct with fields @@ -226,7 +229,7 @@ pub fn setter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[inline] fn check_if_auth_based_attr_is_present(f: &syn::Field, ident: &str) -> bool { for i in f.attrs.iter() { - if i.path.is_ident(ident) { + if i.path().is_ident(ident) { return true; } } @@ -460,7 +463,8 @@ pub fn api_error_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre #[proc_macro_derive(PaymentOperation, attributes(operation))] pub fn operation_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); - macros::operation_derive_inner(input).unwrap_or_else(|err| err.to_compile_error().into()) + macros::operation::operation_derive_inner(input) + .unwrap_or_else(|err| err.to_compile_error().into()) } /// Generates different schemas with the ability to mark few fields as mandatory for certain schema diff --git a/crates/router_derive/src/macros.rs b/crates/router_derive/src/macros.rs index 86501f054a59..9a8e514c5c11 100644 --- a/crates/router_derive/src/macros.rs +++ b/crates/router_derive/src/macros.rs @@ -13,11 +13,8 @@ use syn::DeriveInput; pub(crate) use self::{ api_error::api_error_derive_inner, - diesel::{ - diesel_enum_attribute_inner, diesel_enum_derive_inner, diesel_enum_text_derive_inner, - }, + diesel::{diesel_enum_derive_inner, diesel_enum_text_derive_inner}, generate_schema::polymorphic_macro_derive_inner, - operation::operation_derive_inner, }; pub(crate) fn debug_as_display_inner(ast: &DeriveInput) -> syn::Result { diff --git a/crates/router_derive/src/macros/api_error/helpers.rs b/crates/router_derive/src/macros/api_error/helpers.rs index e1e2a09eacb1..5781d786ee56 100644 --- a/crates/router_derive/src/macros/api_error/helpers.rs +++ b/crates/router_derive/src/macros/api_error/helpers.rs @@ -1,3 +1,5 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{ parse::Parse, spanned::Spanned, DeriveInput, Field, Fields, LitStr, Token, TypePath, Variant, }; @@ -38,10 +40,10 @@ impl Parse for EnumMeta { } } -impl Spanned for EnumMeta { - fn span(&self) -> proc_macro2::Span { +impl ToTokens for EnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::ErrorTypeEnum { keyword, .. } => keyword.span(), + Self::ErrorTypeEnum { keyword, .. } => keyword.to_tokens(tokens), } } } @@ -143,13 +145,13 @@ impl Parse for VariantMeta { } } -impl Spanned for VariantMeta { - fn span(&self) -> proc_macro2::Span { +impl ToTokens for VariantMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::ErrorType { keyword, .. } => keyword.span, - Self::Code { keyword, .. } => keyword.span, - Self::Message { keyword, .. } => keyword.span, - Self::Ignore { keyword, .. } => keyword.span, + Self::ErrorType { keyword, .. } => keyword.to_tokens(tokens), + Self::Code { keyword, .. } => keyword.to_tokens(tokens), + Self::Message { keyword, .. } => keyword.to_tokens(tokens), + Self::Ignore { keyword, .. } => keyword.to_tokens(tokens), } } } diff --git a/crates/router_derive/src/macros/diesel.rs b/crates/router_derive/src/macros/diesel.rs index 07957bef785e..d15eecf41b9c 100644 --- a/crates/router_derive/src/macros/diesel.rs +++ b/crates/router_derive/src/macros/diesel.rs @@ -1,10 +1,8 @@ -#![allow(clippy::use_self)] -use darling::FromMeta; use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote}; -use syn::{AttributeArgs, Data, DeriveInput, ItemEnum}; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse::Parse, Data, DeriveInput, ItemEnum}; -use crate::macros::helpers::non_enum_error; +use crate::macros::helpers; pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; @@ -12,10 +10,11 @@ pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result (), - _ => return Err(non_enum_error()), - } + _ => return Err(helpers::non_enum_error()), + }; Ok(quote! { + #[automatically_derived] impl #impl_generics ::diesel::serialize::ToSql<::diesel::sql_types::Text, ::diesel::pg::Pg> for #name #ty_generics #where_clause @@ -42,18 +41,20 @@ pub(crate) fn diesel_enum_text_derive_inner(ast: &DeriveInput) -> syn::Result syn::Result { +pub(crate) fn diesel_enum_db_enum_derive_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); match &ast.data { Data::Enum(_) => (), - _ => return Err(non_enum_error()), - } + _ => return Err(helpers::non_enum_error()), + }; let struct_name = format_ident!("Db{name}"); let type_name = format!("{name}"); + Ok(quote! { + #[derive(::core::clone::Clone, ::core::marker::Copy, ::core::fmt::Debug, ::diesel::QueryId, ::diesel::SqlType)] #[diesel(postgres_type(name = #type_name))] pub struct #struct_name; @@ -84,45 +85,138 @@ pub(crate) fn diesel_enum_derive_inner(ast: &DeriveInput) -> syn::Result syn::Result { - #[derive(FromMeta, Debug)] - enum StorageType { - PgEnum, - Text, +mod diesel_keyword { + use syn::custom_keyword; + + custom_keyword!(storage_type); + custom_keyword!(db_enum); + custom_keyword!(text); +} + +#[derive(Debug, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum StorageType { + /// Store the Enum as Text value in the database + Text, + /// Store the Enum as Enum in the database. This requires a corresponding enum to be created + /// in the database with the same name + DbEnum, +} + +#[derive(Debug)] +pub enum DieselEnumMeta { + StorageTypeEnum { + keyword: diesel_keyword::storage_type, + value: StorageType, + }, +} + +impl Parse for StorageType { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for storage_type: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), + ) + }) + } +} + +impl DieselEnumMeta { + pub fn get_storage_type(&self) -> &StorageType { + match self { + Self::StorageTypeEnum { value, .. } => value, + } } +} - #[derive(FromMeta, Debug)] - struct StorageTypeArgs { - storage_type: StorageType, +impl Parse for DieselEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(diesel_keyword::storage_type) { + let keyword = input.parse()?; + input.parse::()?; + let value = input.parse()?; + Ok(Self::StorageTypeEnum { keyword, value }) + } else { + Err(lookahead.error()) + } } +} - let storage_type_args = match StorageTypeArgs::from_list(args) { - Ok(v) => v, - Err(_) => { - return Err(syn::Error::new( - Span::call_site(), - "Expected storage_type of text or pg_enum", - )); +impl ToTokens for DieselEnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::StorageTypeEnum { keyword, .. } => keyword.to_tokens(tokens), } - }; + } +} + +trait DieselDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl DieselDeriveInputExt for DeriveInput { + fn get_metadata(&self) -> syn::Result> { + helpers::get_metadata_inner("storage_type", &self.attrs) + } +} + +pub(crate) fn diesel_enum_derive_inner(ast: &DeriveInput) -> syn::Result { + let storage_type = ast.get_metadata()?; + + match storage_type + .first() + .ok_or(syn::Error::new( + Span::call_site(), + "Storage type must be specified", + ))? + .get_storage_type() + { + StorageType::Text => diesel_enum_text_derive_inner(ast), + StorageType::DbEnum => diesel_enum_db_enum_derive_inner(ast), + } +} - match storage_type_args.storage_type { - StorageType::PgEnum => { - let name = &item.ident; - let type_name = format_ident!("Db{name}"); - Ok(quote! { - #[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnum) ] - #[diesel(sql_type = #type_name)] +/// Based on the storage type, derive appropriate diesel traits +/// This will add the appropriate #[diesel(sql_type)] +/// Since the `FromSql` and `ToSql` have to be derived for all the enums, this will add the +/// `DieselEnum` derive trait. +pub(crate) fn diesel_enum_attribute_macro( + diesel_enum_meta: DieselEnumMeta, + item: &ItemEnum, +) -> syn::Result { + let diesel_derives = + quote!(#[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnum) ]); + + match diesel_enum_meta { + DieselEnumMeta::StorageTypeEnum { + value: storage_type, + .. + } => match storage_type { + StorageType::Text => Ok(quote! { + #diesel_derives + #[diesel(sql_type = ::diesel::sql_types::Text)] + #[storage_type(storage_type = "text")] #item - }) - } - StorageType::Text => Ok(quote! { - #[derive(diesel::AsExpression, diesel::FromSqlRow, router_derive::DieselEnumText) ] - #[diesel(sql_type = ::diesel::sql_types::Text)] - #item - }), + }), + StorageType::DbEnum => { + let name = &item.ident; + let type_name = format_ident!("Db{name}"); + Ok(quote! { + #diesel_derives + #[diesel(sql_type = #type_name)] + #[storage_type(storage_type= "db_enum")] + #item + }) + } + }, } } diff --git a/crates/router_derive/src/macros/generate_schema.rs b/crates/router_derive/src/macros/generate_schema.rs index 2669106cecd4..05d5b2919e11 100644 --- a/crates/router_derive/src/macros/generate_schema.rs +++ b/crates/router_derive/src/macros/generate_schema.rs @@ -42,12 +42,14 @@ pub fn polymorphic_macro_derive_inner( let (mandatory_attribute, other_attributes) = field .attrs .iter() - .partition::, _>(|attribute| attribute.path.is_ident("mandatory_in")); + .partition::, _>(|attribute| attribute.path().is_ident("mandatory_in")); // Other attributes ( schema ) are to be printed as is other_attributes .iter() - .filter(|attribute| attribute.path.is_ident("schema") || attribute.path.is_ident("doc")) + .filter(|attribute| { + attribute.path().is_ident("schema") || attribute.path().is_ident("doc") + }) .for_each(|attribute| { // Since attributes will be modified, the field should not contain any attributes // So create a field, with previous attributes removed diff --git a/crates/router_derive/src/macros/helpers.rs b/crates/router_derive/src/macros/helpers.rs index 94005453f8de..b6490c4d6298 100644 --- a/crates/router_derive/src/macros/helpers.rs +++ b/crates/router_derive/src/macros/helpers.rs @@ -23,13 +23,24 @@ pub(super) fn syn_error(span: Span, message: &str) -> syn::Error { syn::Error::new(span, message) } +/// Get all the variants of a enum in the form of a string +pub fn get_possible_values_for_enum() -> String +where + T: strum::IntoEnumIterator + ToString, +{ + T::iter() + .map(|variants| variants.to_string()) + .collect::>() + .join(", ") +} + pub(super) fn get_metadata_inner<'a, T: Parse + Spanned>( ident: &str, attrs: impl IntoIterator, ) -> syn::Result> { attrs .into_iter() - .filter(|attr| attr.path.is_ident(ident)) + .filter(|attr| attr.path().is_ident(ident)) .try_fold(Vec::new(), |mut vec, attr| { vec.extend(attr.parse_args_with(Punctuated::::parse_terminated)?); Ok(vec) diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index fb0ef35ef587..370e03b984ba 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -1,25 +1,27 @@ -use std::collections::HashMap; +use std::str::FromStr; use proc_macro2::{Span, TokenStream}; -use quote::quote; -use syn::{self, spanned::Spanned, DeriveInput, Lit, Meta, MetaNameValue, NestedMeta}; +use quote::{quote, ToTokens}; +use strum::IntoEnumIterator; +use syn::{self, parse::Parse, DeriveInput}; -use crate::macros::helpers; +use crate::macros::helpers::{self}; -#[derive(Debug, Clone, Copy)] -enum Derives { +#[derive(Debug, Clone, Copy, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum Derives { Sync, Cancel, Reject, Capture, - Approvedata, + ApproveData, Authorize, - Authorizedata, - Syncdata, - Canceldata, - Capturedata, + AuthorizeData, + SyncData, + CancelData, + CaptureData, CompleteAuthorizeData, - Rejectdata, + RejectData, SetupMandateData, Start, Verify, @@ -27,31 +29,6 @@ enum Derives { SessionData, } -impl From for Derives { - fn from(s: String) -> Self { - match s.as_str() { - "sync" => Self::Sync, - "cancel" => Self::Cancel, - "reject" => Self::Reject, - "syncdata" => Self::Syncdata, - "authorize" => Self::Authorize, - "approvedata" => Self::Approvedata, - "authorizedata" => Self::Authorizedata, - "canceldata" => Self::Canceldata, - "capture" => Self::Capture, - "capturedata" => Self::Capturedata, - "completeauthorizedata" => Self::CompleteAuthorizeData, - "rejectdata" => Self::Rejectdata, - "start" => Self::Start, - "verify" => Self::Verify, - "setupmandatedata" => Self::SetupMandateData, - "session" => Self::Session, - "sessiondata" => Self::SessionData, - _ => Self::Authorize, - } - } -} - impl Derives { fn to_operation( self, @@ -82,8 +59,9 @@ impl Derives { } } -#[derive(PartialEq, Eq, Hash)] -enum Conversion { +#[derive(Debug, Clone, strum::EnumString, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum Conversion { ValidateRequest, GetTracker, Domain, @@ -93,34 +71,20 @@ enum Conversion { Invalid(String), } -impl From for Conversion { - fn from(s: String) -> Self { - match s.as_str() { - "validate_request" => Self::ValidateRequest, - "get_tracker" => Self::GetTracker, - "domain" => Self::Domain, - "update_tracker" => Self::UpdateTracker, - "post_tracker" => Self::PostUpdateTracker, - "all" => Self::All, - s => Self::Invalid(s.to_string()), - } - } -} - impl Conversion { fn get_req_type(ident: Derives) -> syn::Ident { match ident { Derives::Authorize => syn::Ident::new("PaymentsRequest", Span::call_site()), - Derives::Authorizedata => syn::Ident::new("PaymentsAuthorizeData", Span::call_site()), + Derives::AuthorizeData => syn::Ident::new("PaymentsAuthorizeData", Span::call_site()), Derives::Sync => syn::Ident::new("PaymentsRetrieveRequest", Span::call_site()), - Derives::Syncdata => syn::Ident::new("PaymentsSyncData", Span::call_site()), + Derives::SyncData => syn::Ident::new("PaymentsSyncData", Span::call_site()), Derives::Cancel => syn::Ident::new("PaymentsCancelRequest", Span::call_site()), - Derives::Canceldata => syn::Ident::new("PaymentsCancelData", Span::call_site()), - Derives::Approvedata => syn::Ident::new("PaymentsApproveData", Span::call_site()), + Derives::CancelData => syn::Ident::new("PaymentsCancelData", Span::call_site()), + Derives::ApproveData => syn::Ident::new("PaymentsApproveData", Span::call_site()), Derives::Reject => syn::Ident::new("PaymentsRejectRequest", Span::call_site()), - Derives::Rejectdata => syn::Ident::new("PaymentsRejectData", Span::call_site()), + Derives::RejectData => syn::Ident::new("PaymentsRejectData", Span::call_site()), Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()), - Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()), + Derives::CaptureData => syn::Ident::new("PaymentsCaptureData", Span::call_site()), Derives::CompleteAuthorizeData => { syn::Ident::new("CompleteAuthorizeData", Span::call_site()) } @@ -231,103 +195,206 @@ impl Conversion { } } -fn find_operation_attr(a: &[syn::Attribute]) -> syn::Result { - a.iter() - .find(|a| { - a.path - .get_ident() - .map(|ident| *ident == "operation") - .unwrap_or(false) +mod operations_keyword { + use syn::custom_keyword; + + custom_keyword!(operations); + custom_keyword!(flow); +} + +#[derive(Debug)] +pub enum OperationsEnumMeta { + Operations { + keyword: operations_keyword::operations, + value: Vec, + }, + Flow { + keyword: operations_keyword::flow, + value: Vec, + }, +} + +#[derive(Clone)] +pub struct OperationProperties { + operations: Vec, + flows: Vec, +} + +fn get_operation_properties( + operation_enums: Vec, +) -> syn::Result { + let mut operations = vec![]; + let mut flows = vec![]; + + for operation in operation_enums { + match operation { + OperationsEnumMeta::Operations { value, .. } => { + operations = value; + } + OperationsEnumMeta::Flow { value, .. } => { + flows = value; + } + } + } + + if operations.is_empty() { + Err(syn::Error::new( + Span::call_site(), + "atleast one operation must be specitied", + ))?; + } + + if flows.is_empty() { + Err(syn::Error::new( + Span::call_site(), + "atleast one flow must be specitied", + ))?; + } + + Ok(OperationProperties { operations, flows }) +} + +impl Parse for Derives { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for flow: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), + ) }) - .cloned() - .ok_or_else(|| { - helpers::syn_error( - Span::call_site(), - "Cannot find attribute 'operation' in the macro", + } +} + +impl Parse for Conversion { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let text = input.parse::()?; + let value = text.value(); + + value.as_str().parse().map_err(|_| { + syn::Error::new_spanned( + &text, + format!( + "Unexpected value for operation: `{value}`. Possible values are `{}`", + helpers::get_possible_values_for_enum::() + ), ) }) + } +} + +fn parse_list_string(list_string: String, keyword: &str) -> syn::Result> +where + T: FromStr + IntoEnumIterator + ToString, +{ + list_string + .split(',') + .map(str::trim) + .map(T::from_str) + .map(|result| { + result.map_err(|_| { + syn::Error::new( + Span::call_site(), + format!( + "Unexpected {keyword}, possible values are {}", + helpers::get_possible_values_for_enum::() + ), + ) + }) + }) + .collect() +} + +fn get_conversions(input: syn::parse::ParseStream<'_>) -> syn::Result> { + let lit_str_list = input.parse::()?; + parse_list_string(lit_str_list.value(), "operation") +} + +fn get_derives(input: syn::parse::ParseStream<'_>) -> syn::Result> { + let lit_str_list = input.parse::()?; + parse_list_string(lit_str_list.value(), "flow") } -fn find_value(v: &NestedMeta) -> Option<(String, Vec)> { - match v { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - ref path, - eq_token: _, - lit: Lit::Str(ref litstr), - })) => { - let key = path.get_ident()?.to_string(); - Some(( - key, - litstr.value().split(',').map(ToString::to_string).collect(), - )) +impl Parse for OperationsEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(operations_keyword::operations) { + let keyword = input.parse()?; + input.parse::()?; + let value = get_conversions(input)?; + Ok(Self::Operations { keyword, value }) + } else if lookahead.peek(operations_keyword::flow) { + let keyword = input.parse()?; + input.parse::()?; + let value = get_derives(input)?; + Ok(Self::Flow { keyword, value }) + } else { + Err(lookahead.error()) } - _ => None, } } -fn find_properties(attr: &syn::Attribute) -> syn::Result>> { - let meta = attr.parse_meta(); - match meta { - Ok(syn::Meta::List(syn::MetaList { - ref path, - paren_token: _, - nested, - })) => { - path.get_ident().map(|i| i == "operation").ok_or_else(|| { - helpers::syn_error(path.span(), "Attribute 'operation' was not found") - })?; - Ok(HashMap::from_iter(nested.iter().filter_map(find_value))) +trait OperationsDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl OperationsDeriveInputExt for DeriveInput { + fn get_metadata(&self) -> syn::Result> { + helpers::get_metadata_inner("operation", &self.attrs) + } +} + +impl ToTokens for OperationsEnumMeta { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Operations { keyword, .. } => keyword.to_tokens(tokens), + Self::Flow { keyword, .. } => keyword.to_tokens(tokens), } - _ => Err(helpers::syn_error( - attr.span(), - "No attributes were found. Expected format is ops=..,flow=..", - )), } } pub fn operation_derive_inner(input: DeriveInput) -> syn::Result { let struct_name = &input.ident; - let op = find_operation_attr(&input.attrs)?; - let prop = find_properties(&op)?; - let ops = prop.get("ops").ok_or_else(|| { - helpers::syn_error( - op.span(), - "Invalid properties. Property 'ops' was not found", - ) - })?; - let flow = prop.get("flow").ok_or_else(|| { - helpers::syn_error( - op.span(), - "Invalid properties. Property 'flow' was not found", - ) - })?; - let current_crate = syn::Ident::new( - &prop - .get("crate") - .map(|v| v.join("")) - .unwrap_or_else(|| String::from("crate")), - Span::call_site(), - ); - - let trait_derive = flow.iter().map(|derive| { - let derive: Derives = derive.to_owned().into(); - let fns = ops.iter().map(|t| { - let con: Conversion = t.to_owned().into(); - con.to_function(derive) - }); - derive.to_operation(fns, struct_name) - }); - let ref_trait_derive = flow.iter().map(|derive| { - let derive: Derives = derive.to_owned().into(); - let fns = ops.iter().map(|t| { - let con: Conversion = t.to_owned().into(); - con.to_ref_function(derive) - }); - derive.to_ref_operation(fns, struct_name) - }); + let operations_meta = input.get_metadata()?; + let operation_properties = get_operation_properties(operations_meta)?; + + let current_crate = syn::Ident::new("crate", Span::call_site()); + + let trait_derive = operation_properties + .clone() + .flows + .into_iter() + .map(|derive| { + let fns = operation_properties + .operations + .iter() + .map(|conversion| conversion.to_function(derive)); + derive.to_operation(fns, struct_name) + }) + .collect::>(); + + let ref_trait_derive = operation_properties + .flows + .into_iter() + .map(|derive| { + let fns = operation_properties + .operations + .iter() + .map(|conversion| conversion.to_ref_function(derive)); + derive.to_ref_operation(fns, struct_name) + }) + .collect::>(); + let trait_derive = quote! { #(#ref_trait_derive)* #(#trait_derive)* }; + let output = quote! { const _: () = { use #current_crate::core::errors::RouterResult; diff --git a/crates/router_derive/src/macros/try_get_enum.rs b/crates/router_derive/src/macros/try_get_enum.rs index 3a534b080df1..f607b7f06c9c 100644 --- a/crates/router_derive/src/macros/try_get_enum.rs +++ b/crates/router_derive/src/macros/try_get_enum.rs @@ -1,21 +1,62 @@ use proc_macro2::Span; -use syn::punctuated::Punctuated; +use quote::ToTokens; +use syn::{parse::Parse, punctuated::Punctuated}; + +mod try_get_keyword { + use syn::custom_keyword; + + custom_keyword!(error_type); +} + +#[derive(Debug)] +pub struct TryGetEnumMeta { + error_type: syn::Ident, + variant: syn::Ident, +} + +impl Parse for TryGetEnumMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let error_type = input.parse()?; + _ = input.parse::()?; + let variant = input.parse()?; + + Ok(Self { + error_type, + variant, + }) + } +} + +trait TryGetDeriveInputExt { + /// Get all the error metadata associated with an enum. + fn get_metadata(&self) -> syn::Result>; +} + +impl TryGetDeriveInputExt for syn::DeriveInput { + fn get_metadata(&self) -> syn::Result> { + super::helpers::get_metadata_inner("error", &self.attrs) + } +} + +impl ToTokens for TryGetEnumMeta { + fn to_tokens(&self, _: &mut proc_macro2::TokenStream) {} +} /// Try and get the variants for an enum pub fn try_get_enum_variant( input: syn::DeriveInput, ) -> Result { let name = &input.ident; + let parsed_error_type = input.get_metadata()?; + + let (error_type, error_variant) = parsed_error_type + .first() + .ok_or(syn::Error::new( + Span::call_site(), + "One error should be specified", + )) + .map(|error_struct| (&error_struct.error_type, &error_struct.variant))?; - let error_attr = input - .attrs - .iter() - .find(|attr| attr.path.is_ident("error")) - .ok_or(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Unable to find attribute error. Expected #[error(..)]", - ))?; - let (error_type, error_variant) = get_error_type_and_variant(error_attr)?; let (impl_generics, generics, where_clause) = input.generics.split_for_impl(); let variants = get_enum_variants(&input.data)?; @@ -49,52 +90,6 @@ pub fn try_get_enum_variant( Ok(expanded) } -/// Parses the attribute #[error(ErrorType(ErrorVariant))] -fn get_error_type_and_variant(attr: &syn::Attribute) -> syn::Result<(syn::Ident, syn::Path)> { - let meta = attr.parse_meta()?; - let metalist = match meta { - syn::Meta::List(list) => list, - _ => { - return Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant)]", - )) - } - }; - - for meta in metalist.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::List(meta)) = meta { - let error_type = meta - .path - .get_ident() - .ok_or(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant))]", - )) - .cloned()?; - let error_variant = get_error_variant(meta)?; - return Ok((error_type, error_variant)); - }; - } - - Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format #[error(ErrorType(ErrorVariant))]", - )) -} - -fn get_error_variant(meta: &syn::MetaList) -> syn::Result { - for meta in meta.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::Path(meta)) = meta { - return Ok(meta.clone()); - } - } - Err(super::helpers::syn_error( - proc_macro2::Span::call_site(), - "Invalid attribute format expected #[error(ErrorType(ErrorVariant))]", - )) -} - /// Get variants from Enum fn get_enum_variants(data: &syn::Data) -> syn::Result> { if let syn::Data::Enum(syn::DataEnum { variants, .. }) = data { diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 3eadd8b83ade..c45282da7f5e 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -60,7 +60,7 @@ pub enum KvOperation<'a, S: serde::Serialize + Debug> { } #[derive(TryGetEnumVariant)] -#[error(RedisError(UnknownResult))] +#[error(RedisError::UnknownResult)] pub enum KvResult { HGet(T), Get(T), From 6954de77a0fda14d87b79ec7ceee7cc8f1c491db Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Wed, 22 Nov 2023 16:34:17 +0530 Subject: [PATCH 043/443] fix: kv logs when KeyNotSet is returned (#2928) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/redis_interface/src/errors.rs | 2 ++ crates/storage_impl/src/redis/kv_store.rs | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/redis_interface/src/errors.rs b/crates/redis_interface/src/errors.rs index 213fb799892e..5289ec4fec47 100644 --- a/crates/redis_interface/src/errors.rs +++ b/crates/redis_interface/src/errors.rs @@ -8,6 +8,8 @@ pub enum RedisError { InvalidConfiguration(String), #[error("Failed to set key value in Redis")] SetFailed, + #[error("Failed to set key value in Redis. Duplicate value")] + SetNxFailed, #[error("Failed to set key value with expiry in Redis")] SetExFailed, #[error("Failed to set expiry for key value in Redis")] diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index c45282da7f5e..9339b11a9b9c 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, sync::Arc}; use common_utils::errors::CustomResult; +use error_stack::IntoReport; use redis_interface::errors::RedisError; use router_derive::TryGetEnumVariant; use router_env::logger; @@ -145,8 +146,10 @@ where store .push_to_drainer_stream::(sql, partition_key) .await?; + Ok(KvResult::HSetNx(result)) + } else { + Err(RedisError::SetNxFailed).into_report() } - Ok(KvResult::HSetNx(result)) } KvOperation::SetNx(value, sql) => { @@ -160,9 +163,10 @@ where store .push_to_drainer_stream::(sql, partition_key) .await?; + Ok(KvResult::SetNx(result)) + } else { + Err(RedisError::SetNxFailed).into_report() } - - Ok(KvResult::SetNx(result)) } KvOperation::Get => { From 341374b8e5eced329587b93cbb6bd58e16dd9932 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:54:40 +0530 Subject: [PATCH 044/443] refactor(mca): Add Serialization for `ConnectorAuthType` (#2945) --- crates/router/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 203d4e30bf9a..a03e41650408 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -904,7 +904,7 @@ pub struct ResponseRouterData { } // Different patterns of authentication. -#[derive(Default, Debug, Clone, serde::Deserialize)] +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] #[serde(tag = "auth_type")] pub enum ConnectorAuthType { TemporaryAuth, From 4e15d7792e3167de170c3d8310f33419f4dfb0db Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:00:30 +0530 Subject: [PATCH 045/443] feat(routing): Routing prometheus metrics (#2870) Co-authored-by: Prajjwal Kumar --- crates/router/src/core/metrics.rs | 33 ++++++++++++++++++++++ crates/router/src/core/routing.rs | 47 +++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/metrics.rs b/crates/router/src/core/metrics.rs index eb8a5be8d4ad..c5a05a169c75 100644 --- a/crates/router/src/core/metrics.rs +++ b/crates/router/src/core/metrics.rs @@ -44,3 +44,36 @@ counter_metric!( WEBHOOK_EVENT_TYPE_IDENTIFICATION_FAILURE_COUNT, GLOBAL_METER ); + +counter_metric!(ROUTING_CREATE_REQUEST_RECEIVED, GLOBAL_METER); +counter_metric!(ROUTING_CREATE_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_MERCHANT_DICTIONARY_RETRIEVE, GLOBAL_METER); +counter_metric!( + ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_LINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_LINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_DEFAULT_CONFIG, GLOBAL_METER); +counter_metric!( + ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_RETRIEVE_LINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UNLINK_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE, GLOBAL_METER); +counter_metric!(ROUTING_UPDATE_CONFIG_FOR_PROFILE, GLOBAL_METER); +counter_metric!( + ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE, + GLOBAL_METER +); +counter_metric!(ROUTING_RETRIEVE_CONFIG_FOR_PROFILE, GLOBAL_METER); +counter_metric!( + ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE, + GLOBAL_METER +); diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 4171c3385637..e9ddcb4a5632 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -19,7 +19,7 @@ use crate::{ consts, core::{ errors::{RouterResponse, StorageErrorExt}, - utils as core_utils, + metrics, utils as core_utils, }, routes::AppState, types::domain, @@ -35,6 +35,7 @@ pub async fn retrieve_merchant_routing_dictionary( merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery, ) -> RouterResponse { + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(&metrics::CONTEXT, 1, &[]); #[cfg(feature = "business_profile_routing")] { let routing_metadata = state @@ -51,11 +52,18 @@ pub async fn retrieve_merchant_routing_dictionary( .map(ForeignInto::foreign_into) .collect::>(); + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); Ok(service_api::ApplicationResponse::Json( routing_types::RoutingKind::RoutingAlgorithm(result), )) } #[cfg(not(feature = "business_profile_routing"))] + metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); + #[cfg(not(feature = "business_profile_routing"))] Ok(service_api::ApplicationResponse::Json( routing_types::RoutingKind::Config( helpers::get_merchant_routing_dictionary( @@ -73,6 +81,7 @@ pub async fn create_routing_config( key_store: domain::MerchantKeyStore, request: routing_types::RoutingConfigRequest, ) -> RouterResponse { + metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let name = request @@ -147,6 +156,7 @@ pub async fn create_routing_config( let new_record = record.foreign_into(); + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(new_record)) } @@ -213,6 +223,7 @@ pub async fn create_routing_config( ) .await?; + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(new_record)) } } @@ -223,6 +234,7 @@ pub async fn link_routing_config( #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, algorithm_id: String, ) -> RouterResponse { + metrics::ROUTING_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -268,6 +280,7 @@ pub async fn link_routing_config( helpers::update_business_profile_active_algorithm_ref(db, business_profile, routing_ref) .await?; + metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_algorithm.foreign_into(), )) @@ -317,6 +330,7 @@ pub async fn link_routing_config( .await?; helpers::update_merchant_active_algorithm_ref(db, &key_store, routing_ref).await?; + metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -326,6 +340,7 @@ pub async fn retrieve_routing_config( merchant_account: domain::MerchantAccount, algorithm_id: RoutingAlgorithmId, ) -> RouterResponse { + metrics::ROUTING_RETRIEVE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -350,6 +365,8 @@ pub async fn retrieve_routing_config( .foreign_try_into() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to parse routing algorithm")?; + + metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } @@ -387,6 +404,7 @@ pub async fn retrieve_routing_config( modified_at: record.modified_at, }; + metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -396,6 +414,7 @@ pub async fn unlink_routing_config( #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, #[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest, ) -> RouterResponse { + metrics::ROUTING_UNLINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] { @@ -451,6 +470,12 @@ pub async fn unlink_routing_config( routing_algorithm, ) .await?; + + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); Ok(service_api::ApplicationResponse::Json(response)) } None => Err(errors::ApiErrorResponse::PreconditionFailed { @@ -559,6 +584,7 @@ pub async fn unlink_routing_config( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in merchant account")?; + metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -568,6 +594,7 @@ pub async fn update_default_routing_config( merchant_account: domain::MerchantAccount, updated_config: Vec, ) -> RouterResponse> { + metrics::ROUTING_UPDATE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let default_config = helpers::get_merchant_default_config(db, &merchant_account.merchant_id).await?; @@ -606,6 +633,7 @@ pub async fn update_default_routing_config( ) .await?; + metrics::ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(updated_config)) } @@ -613,11 +641,19 @@ pub async fn retrieve_default_routing_config( state: AppState, merchant_account: domain::MerchantAccount, ) -> RouterResponse> { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); helpers::get_merchant_default_config(db, &merchant_account.merchant_id) .await - .map(service_api::ApplicationResponse::Json) + .map(|conn_choice| { + metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[], + ); + service_api::ApplicationResponse::Json(conn_choice) + }) } pub async fn retrieve_linked_routing_config( @@ -625,6 +661,7 @@ pub async fn retrieve_linked_routing_config( merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveLinkQuery, ) -> RouterResponse { + metrics::ROUTING_RETRIEVE_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); #[cfg(feature = "business_profile_routing")] @@ -672,6 +709,7 @@ pub async fn retrieve_linked_routing_config( } } + metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms), )) @@ -718,6 +756,7 @@ pub async fn retrieve_linked_routing_config( routing_types::RoutingRetrieveResponse { algorithm }, ); + metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(response)) } } @@ -726,6 +765,7 @@ pub async fn retrieve_default_routing_config_for_profiles( state: AppState, merchant_account: domain::MerchantAccount, ) -> RouterResponse> { + metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let all_profiles = db @@ -755,6 +795,7 @@ pub async fn retrieve_default_routing_config_for_profiles( ) .collect::>(); + metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json(default_configs)) } @@ -764,6 +805,7 @@ pub async fn update_default_routing_config_for_profile( updated_config: Vec, profile_id: String, ) -> RouterResponse { + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let business_profile = core_utils::validate_and_get_business_profile( @@ -829,6 +871,7 @@ pub async fn update_default_routing_config_for_profile( ) .await?; + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( routing_types::ProfileDefaultRoutingConfig { profile_id: business_profile.profile_id, From 998948953ab8a444aca79957f48e7cfb3066c334 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:11:02 +0530 Subject: [PATCH 046/443] feat(payment_methods): add support for tokenising bank details and fetching masked details while listing (#2585) Co-authored-by: shashank_attarde Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 10 ++ .../router/src/core/payment_methods/cards.rs | 114 +++++++++++++++++- crates/router/src/openapi.rs | 1 + openapi/openapi_spec.json | 19 +++ 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c40dffe4cf31..8710c69aa5c6 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -811,10 +811,20 @@ pub struct CustomerPaymentMethod { #[schema(value_type = Option)] pub bank_transfer: Option, + /// Masked bank details from PM auth services + #[schema(example = json!({"mask": "0000"}))] + pub bank: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct MaskedBankDetails { + pub mask: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentMethodId { pub payment_method_id: String, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 60fd3f315ea6..85a0ca5f2441 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -7,9 +7,10 @@ use api_models::{ admin::{self, PaymentMethodsEnabled}, enums::{self as api_enums}, payment_methods::{ - CardDetailsPaymentMethod, CardNetworkTypes, PaymentExperienceTypes, PaymentMethodsData, - RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate, - ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, + BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails, + PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, + ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes, + ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, surcharge_decision_configs as api_surcharge_decision_configs, @@ -2210,6 +2211,22 @@ pub async fn list_customer_payment_method( ) } + enums::PaymentMethod::BankDebit => { + // Retrieve the pm_auth connector details so that it can be tokenized + let bank_account_connector_details = get_bank_account_connector_details(&pm, key) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + if let Some(connector_details) = bank_account_connector_details { + let token_data = PaymentTokenData::AuthBankDebit(connector_details); + (None, None, token_data) + } else { + continue; + } + } + _ => ( None, None, @@ -2217,6 +2234,18 @@ pub async fn list_customer_payment_method( ), }; + // Retrieve the masked bank details to be sent as a response + let bank_details = if pm.payment_method == enums::PaymentMethod::BankDebit { + get_masked_bank_details(&pm, key) + .await + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + } else { + None + }; + //Need validation for enabled payment method ,querying MCA let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), @@ -2232,6 +2261,7 @@ pub async fn list_customer_payment_method( payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), bank_transfer: pmd, + bank: bank_details, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2356,6 +2386,84 @@ pub async fn get_lookup_key_from_locker( Ok(resp) } +async fn get_masked_bank_details( + pm: &payment_method::PaymentMethod, + key: &[u8], +) -> errors::RouterResult> { + let payment_method_data = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank details")? + .map(|x| x.into_inner().expose()) + .map( + |v| -> Result> { + v.parse_value::("PaymentMethodsData") + .change_context(errors::StorageError::DeserializationFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize Payment Method Auth config") + }, + ) + .transpose()?; + + match payment_method_data { + Some(pmd) => match pmd { + PaymentMethodsData::Card(_) => Ok(None), + PaymentMethodsData::BankDetails(bank_details) => Ok(Some(MaskedBankDetails { + mask: bank_details.mask, + })), + }, + None => Err(errors::ApiErrorResponse::InternalServerError.into()) + .attach_printable("Unable to fetch payment method data"), + } +} + +async fn get_bank_account_connector_details( + pm: &payment_method::PaymentMethod, + key: &[u8], +) -> errors::RouterResult> { + let payment_method_data = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank details")? + .map(|x| x.into_inner().expose()) + .map( + |v| -> Result> { + v.parse_value::("PaymentMethodsData") + .change_context(errors::StorageError::DeserializationFailed) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize Payment Method Auth config") + }, + ) + .transpose()?; + + match payment_method_data { + Some(pmd) => match pmd { + PaymentMethodsData::Card(_) => Err(errors::ApiErrorResponse::UnprocessableEntity { + message: "Card is not a valid entity".to_string(), + }) + .into_report(), + PaymentMethodsData::BankDetails(bank_details) => { + let connector_details = bank_details + .connector_details + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + Ok(Some(BankAccountConnectorDetails { + connector: connector_details.connector.clone(), + account_id: connector_details.account_id.clone(), + mca_id: connector_details.mca_id.clone(), + access_token: connector_details.access_token.clone(), + })) + } + }, + None => Err(errors::ApiErrorResponse::InternalServerError.into()) + .attach_printable("Unable to fetch payment method data"), + } +} + #[cfg(feature = "payouts")] pub async fn get_lookup_key_for_payout_method( state: &routes::AppState, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 04ef90546cfa..d191890b8cdb 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -315,6 +315,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, api_models::payment_methods::RequiredFieldInfo, + api_models::payment_methods::MaskedBankDetails, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index df9df43a43ee..056601ac707d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4828,6 +4828,14 @@ ], "nullable": true }, + "bank": { + "allOf": [ + { + "$ref": "#/components/schemas/MaskedBankDetails" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -6434,6 +6442,17 @@ } ] }, + "MaskedBankDetails": { + "type": "object", + "required": [ + "mask" + ], + "properties": { + "mask": { + "type": "string" + } + } + }, "MbWayRedirection": { "type": "object", "required": [ From f4d534c626923d16cc00348366717d5d3095024f Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:50:22 +0530 Subject: [PATCH 047/443] ci(postman): Add delay for checkout refunds test cases (#2947) --- .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create Copy/event.prerequest.js | 3 + .../Refunds - Create Copy/.event.meta.json | 5 -- .../Refunds - Create Copy/event.test.js | 55 ------------------- .../Refunds - Create Copy/request.json | 42 -------------- .../Refunds - Create Copy/response.json | 1 - .../Refunds - Retrieve Copy/.event.meta.json | 5 -- .../Refunds - Retrieve Copy/event.test.js | 50 ----------------- .../Refunds - Retrieve Copy/request.json | 27 --------- .../Refunds - Retrieve Copy/response.json | 1 - .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create-copy/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + .../Refunds - Create/event.prerequest.js | 3 + 17 files changed, 27 insertions(+), 186 deletions(-) create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json delete mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json deleted file mode 100644 index 688c85746ef1..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/.event.meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eventOrder": [ - "event.test.js" - ] -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js deleted file mode 100644 index c549d5d0c097..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/event.test.js +++ /dev/null @@ -1,55 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/refunds - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[POST]::/refunds - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", - function () { - pm.expect(jsonData.amount).to.eql(540); - }, - ); -} - -// Validate the connector -pm.test("[POST]::/payments - connector", function () { - pm.expect(jsonData.connector).to.eql("checkout"); -}); diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json deleted file mode 100644 index d18aaf8befdf..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/request.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "payment_id": "{{payment_id}}", - "amount": 540, - "reason": "Customer returned product", - "refund_type": "instant", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json deleted file mode 100644 index fe51488c7066..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Create Copy/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json deleted file mode 100644 index 688c85746ef1..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/.event.meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eventOrder": [ - "event.test.js" - ] -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js deleted file mode 100644 index 920a7c47f361..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/event.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Validate status 2xx -pm.test("[GET]::/refunds/:id - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[GET]::/refunds/:id - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "6540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '540'", - function () { - pm.expect(jsonData.amount).to.eql(540); - }, - ); -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json deleted file mode 100644 index 6c28619e8566..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/request.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json deleted file mode 100644 index fe51488c7066..000000000000 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Refunds - Retrieve Copy/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file From 160acc8d49a08d23989a5653205646ae2e329bea Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Wed, 22 Nov 2023 19:59:27 +0530 Subject: [PATCH 048/443] ci(Postman): Fix for automation test failure (#2949) --- .../Recurring Payments - Create/request.json | 2 +- .../Recurring Payments - Create/request.json | 2 +- .../Payments - Capture/event.test.js | 12 ++++++------ .../Payments - Capture/request.json | 2 +- .../Payments - Retrieve-copy/event.test.js | 10 +++++----- .../Payments - Capture/event.test.js | 4 ++-- .../Payments - Retrieve/event.test.js | 4 ++-- .../stripe/Payments/Payments - Update/request.json | 8 ++++++-- 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json index fb25f7ceebf2..13a48ea7de38 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Refund recurring payment/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 6570, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json index 8fc4831ccc32..1cc1bce98079 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario9-Create a recurring payment with greater mandate amount/Recurring Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": true, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 8040, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js index 40445db0fb3f..b06e6c3e1150 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/event.test.js @@ -88,22 +88,22 @@ if (jsonData?.amount) { ); } -// Response body should have value "6000" for "amount_received" +// Response body should have value "6540" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } -// Response body should have value "6540" for "amount_capturable" +// Response body should have value "0" for "amount_capturable" if (jsonData?.amount_capturable) { pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", function () { - pm.expect(jsonData.amount_capturable).to.eql(6540); + pm.expect(jsonData.amount_capturable).to.eql(0); }, ); } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json index 8975575ca40e..8efb99d3c905 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Capture/request.json @@ -18,7 +18,7 @@ } }, "raw_json_formatted": { - "amount_to_capture": 6000, + "amount_to_capture": 6540, "statement_descriptor_name": "Joseph", "statement_descriptor_suffix": "JS" } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js index 0bf6890ea3b6..01f51559ed18 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario12-Save card payment with manual capture Copy/Payments - Retrieve-copy/event.test.js @@ -85,20 +85,20 @@ if (jsonData?.amount) { ); } -// Response body should have value "6000" for "amount_received" +// Response body should have value "6540" for "amount_received" if (jsonData?.amount_received) { pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'", function () { - pm.expect(jsonData.amount_received).to.eql(6000); + pm.expect(jsonData.amount_received).to.eql(6540); }, ); } -// Response body should have value "6540" for "amount_capturable" +// Response body should have value "0" for "amount_capturable" if (jsonData?.amount) { pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'", + "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", function () { pm.expect(jsonData.amount_capturable).to.eql(0); }, diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index 2d7dbc507fb0..f560d84ea730 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -66,9 +66,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js index 5c7196baa4f7..ca68dd7045be 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/stripe/Payments/Payments - Update/request.json b/postman/collection-dir/stripe/Payments/Payments - Update/request.json index 09e3dbb307e6..1809770bd35c 100644 --- a/postman/collection-dir/stripe/Payments/Payments - Update/request.json +++ b/postman/collection-dir/stripe/Payments/Payments - Update/request.json @@ -49,7 +49,9 @@ "city": "San Fransico", "state": "California", "zip": "94122", - "country": "US" + "country": "US", + "first_name": "John", + "last_name": "Doe" } }, "shipping": { @@ -60,7 +62,9 @@ "city": "San Fransico", "state": "California", "zip": "94122", - "country": "US" + "country": "US", + "first_name": "John", + "last_name": "Doe" } }, "statement_descriptor_name": "joseph", From b96052f9c64dd6e49d52ba8befd1f60a843b482a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:33:05 +0000 Subject: [PATCH 049/443] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 4 +- .../bluesnap.postman_collection.json | 32 +-- .../checkout.postman_collection.json | 265 +++++------------- .../stripe.postman_collection.json | 2 +- 4 files changed, 86 insertions(+), 217 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index ad916657948f..33aadeb6f970 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -6634,7 +6634,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -13918,7 +13918,7 @@ "language": "json" } }, - "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8040,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index 82af7ce7bb6c..34ad07ae67a3 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -3166,22 +3166,22 @@ " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", + "// Response body should have value \"6540\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", + "// Response body should have value \"0\" for \"amount_capturable\"", "if (jsonData?.amount_capturable) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", " );", "}", @@ -3210,7 +3210,7 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", @@ -3328,20 +3328,20 @@ " );", "}", "", - "// Response body should have value \"6000\" for \"amount_received\"", + "// Response body should have value \"6540\" for \"amount_received\"", "if (jsonData?.amount_received) {", " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", + " pm.expect(jsonData.amount_received).to.eql(6540);", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", + "// Response body should have value \"0\" for \"amount_capturable\"", "if (jsonData?.amount) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", " function () {", " pm.expect(jsonData.amount_capturable).to.eql(0);", " },", @@ -6596,9 +6596,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", @@ -6743,9 +6743,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index 2bd0ac0f26e0..54892f116a0e 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -730,7 +730,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -1662,6 +1664,17 @@ { "name": "Refunds - Create Copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -4045,200 +4058,6 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] } ] }, @@ -7904,7 +7723,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -8356,6 +8177,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -8550,6 +8382,17 @@ { "name": "Refunds - Create-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -9610,7 +9453,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -11071,7 +10916,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -13453,6 +13300,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -13802,6 +13660,17 @@ { "name": "Refunds - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 9c9a8a5d685c..9bdb5fdb44d9 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -3540,7 +3540,7 @@ "language": "json" } }, - "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id", From 6c15fc312345ed9520accb4d02b12688f98ba1a1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:33:06 +0000 Subject: [PATCH 050/443] chore(version): v1.87.0 --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7b6770d471..f4b86696691e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.87.0 (2023-11-22) + +### Features + +- **api_event_errors:** Error field in APIEvents ([#2808](https://github.com/juspay/hyperswitch/pull/2808)) ([`ce10579`](https://github.com/juspay/hyperswitch/commit/ce10579a729fe4a7d4ab9f1a4cbd38c3ca00e90b)) +- **payment_methods:** Add support for tokenising bank details and fetching masked details while listing ([#2585](https://github.com/juspay/hyperswitch/pull/2585)) ([`9989489`](https://github.com/juspay/hyperswitch/commit/998948953ab8a444aca79957f48e7cfb3066c334)) +- **router:** + - Migrate `payment_method_data` to rust locker only if `payment_method` is card ([#2929](https://github.com/juspay/hyperswitch/pull/2929)) ([`f8261a9`](https://github.com/juspay/hyperswitch/commit/f8261a96e758498a32c988191bf314aa6c752059)) + - Add list payment link support ([#2805](https://github.com/juspay/hyperswitch/pull/2805)) ([`b441a1f`](https://github.com/juspay/hyperswitch/commit/b441a1f2f9d9d84601cf78a6e39145e8fb847593)) +- **routing:** Routing prometheus metrics ([#2870](https://github.com/juspay/hyperswitch/pull/2870)) ([`4e15d77`](https://github.com/juspay/hyperswitch/commit/4e15d7792e3167de170c3d8310f33419f4dfb0db)) + +### Bug Fixes + +- cybersource mandates and fiserv exp year ([#2920](https://github.com/juspay/hyperswitch/pull/2920)) ([`7f74ae9`](https://github.com/juspay/hyperswitch/commit/7f74ae98a1d48eed98341e4505d3801a61e69fc7)) +- Kv logs when KeyNotSet is returned ([#2928](https://github.com/juspay/hyperswitch/pull/2928)) ([`6954de7`](https://github.com/juspay/hyperswitch/commit/6954de77a0fda14d87b79ec7ceee7cc8f1c491db)) + +### Refactors + +- **macros:** Use syn2.0 ([#2890](https://github.com/juspay/hyperswitch/pull/2890)) ([`46e13d5`](https://github.com/juspay/hyperswitch/commit/46e13d54759168ad7667af08d5481ab510e5706a)) +- **mca:** Add Serialization for `ConnectorAuthType` ([#2945](https://github.com/juspay/hyperswitch/pull/2945)) ([`341374b`](https://github.com/juspay/hyperswitch/commit/341374b8e5eced329587b93cbb6bd58e16dd9932)) + +### Testing + +- **postman:** Update postman collection files ([`b96052f`](https://github.com/juspay/hyperswitch/commit/b96052f9c64dd6e49d52ba8befd1f60a843b482a)) + +### Documentation + +- **README:** Update feature support link ([#2894](https://github.com/juspay/hyperswitch/pull/2894)) ([`7d223ee`](https://github.com/juspay/hyperswitch/commit/7d223ee0d1b53c02421ed6bd1b5584362d7a7456)) + +### Miscellaneous Tasks + +- Address Rust 1.74 clippy lints ([#2942](https://github.com/juspay/hyperswitch/pull/2942)) ([`c6a5a85`](https://github.com/juspay/hyperswitch/commit/c6a5a8574825dc333602f4f1cee7e26969eab030)) + +**Full Changelog:** [`v1.86.0...v1.87.0`](https://github.com/juspay/hyperswitch/compare/v1.86.0...v1.87.0) + +- - - + + ## 1.86.0 (2023-11-21) ### Features From f91d4ae11b02def92c1dde743a0c01b5aac5703f Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:01:07 +0530 Subject: [PATCH 051/443] feat(connector): [BANKOFAMERICA] Implement Google Pay (#2940) --- .../connector/bankofamerica/transformers.rs | 256 ++++++++++++++---- 1 file changed, 210 insertions(+), 46 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index a6fa8652b27d..f6cda8ac23ce 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,4 +1,5 @@ use api_models::payments; +use base64::Engine; use common_utils::pii; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -87,6 +88,7 @@ pub struct BankOfAmericaPaymentsRequest { #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { capture: bool, + payment_solution: Option, } #[derive(Debug, Serialize)] @@ -97,10 +99,24 @@ pub struct CaptureOptions { } #[derive(Debug, Serialize)] -pub struct PaymentInformation { +#[serde(rename_all = "camelCase")] +pub struct CardPaymentInformation { card: Card, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentInformation { + fluid_data: FluidData, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymentInformation { + Cards(CardPaymentInformation), + GooglePay(GooglePayPaymentInformation), +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { @@ -112,6 +128,12 @@ pub struct Card { card_type: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { @@ -177,12 +199,165 @@ impl From for String { } } +#[derive(Debug, Serialize)] +pub enum PaymentSolution { + ApplePay, + GooglePay, +} + +impl From for String { + fn from(solution: PaymentSolution) -> Self { + let payment_solution = match solution { + PaymentSolution::ApplePay => "001", + PaymentSolution::GooglePay => "012", + }; + payment_solution.to_string() + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to, + } + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + )> for ProcessingInformation +{ + fn from( + (item, solution): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + ), + ) -> Self { + Self { + capture: matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + ), + payment_solution: solution.map(String::from), + } + } +} + +impl From<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, } +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + + let processing_information = ProcessingInformation::from((item, None)); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, google_pay_data): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let payment_information = PaymentInformation::GooglePay(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE.encode(google_pay_data.tokenization_data.token), + ), + }, + }); + + let processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> for BankOfAmericaPaymentsRequest { @@ -191,52 +366,41 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ccard) => { - let email = item.router_data.request.get_email()?; - let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; - - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency, - }, - bill_to, - }; - let card_issuer = ccard.get_card_issuer(); - let card_type = match card_issuer { - Ok(issuer) => Some(String::from(issuer)), - Err(_) => None, - }; - let payment_information = PaymentInformation { - card: Card { - number: ccard.card_number, - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, - card_type, - }, - }; - - let processing_information = ProcessingInformation { - capture: matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - ), - }; - - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), - }; - - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) - } + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::GooglePay(google_pay_data) => { + Self::try_from((item, google_pay_data)) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePay(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Bank of America"), + ) + .into()), + }, payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Wallet(_) | payments::PaymentMethodData::PayLater(_) | payments::PaymentMethodData::BankRedirect(_) | payments::PaymentMethodData::BankDebit(_) From cb653706066b889eaa9423a6227ce1df954b4759 Mon Sep 17 00:00:00 2001 From: HeetVekariya <91054457+HeetVekariya@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:57:45 +0530 Subject: [PATCH 052/443] refactor(connector): [Payeezy] update error message (#2919) --- .../router/src/connector/payeezy/transformers.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index e2e837929c41..817ab43ac717 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -72,11 +72,9 @@ impl TryFrom for PayeezyCardType { utils::CardIssuer::Maestro | utils::CardIssuer::DinersClub | utils::CardIssuer::JCB - | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()), + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } } @@ -262,11 +260,9 @@ fn get_payment_method_data( | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Payeezy", - } - .into()), + | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Payeezy"), + ))?, } } From e721b06c7077e00458450a4fb98f4497e8227dc6 Mon Sep 17 00:00:00 2001 From: Kaustubh Sharma <123895549+kaustubh1106@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:59:56 +0530 Subject: [PATCH 053/443] refactor(connector): [Worldline] change error message from NotSupported to NotImplemented (#2893) --- crates/router/src/connector/worldline/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 6cb8862f69b1..049453e325ae 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -306,10 +306,9 @@ impl TryFrom for Gateway { utils::CardIssuer::Master => Ok(Self::MasterCard), utils::CardIssuer::Discover => Ok(Self::Discover), utils::CardIssuer::Visa => Ok(Self::Visa), - _ => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "worldline", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("worldline"), + ) .into()), } } From 75eea7e81787f2e0697b930b82a8188193f8d51f Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:03:42 +0530 Subject: [PATCH 054/443] fix: amount_captured goes to 0 for 3ds payments (#2954) --- crates/api_models/src/payments.rs | 12 +++++ crates/router/src/connector/utils.rs | 10 +--- crates/router/src/core/payments/helpers.rs | 16 +++---- .../payments/operations/payment_response.rs | 2 +- crates/router/src/core/refunds.rs | 13 +++--- crates/router/src/types.rs | 46 ++++++++++++++----- .../Payments - Create/request.json | 2 +- 7 files changed, 65 insertions(+), 36 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 508eeb8d7310..a997960edc7e 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -312,6 +312,18 @@ pub struct PaymentsRequest { pub payment_type: Option, } +impl PaymentsRequest { + pub fn get_total_capturable_amount(&self) -> Option { + let surcharge_amount = self + .surcharge_details + .map(|surcharge_details| { + surcharge_details.surcharge_amount + surcharge_details.tax_amount.unwrap_or(0) + }) + .unwrap_or(0); + self.amount + .map(|amount| i64::from(amount) + surcharge_amount) + } +} #[derive( Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema, PartialEq, )] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index a098cef5b778..e096f1878a9c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -111,14 +111,8 @@ where } } enums::AttemptStatus::Charged => { - let captured_amount = if self.request.is_psync() { - payment_data - .payment_attempt - .amount_to_capture - .or(Some(payment_data.payment_attempt.get_total_amount())) - } else { - types::Capturable::get_capture_amount(&self.request) - }; + let captured_amount = + types::Capturable::get_capture_amount(&self.request, payment_data); if Some(payment_data.payment_attempt.get_total_amount()) == captured_amount { enums::AttemptStatus::Charged } else if captured_amount.is_some() { diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4d8daa1fe69d..d813c96ce94b 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -601,19 +601,19 @@ pub fn validate_request_amount_and_amount_to_capture( } } -/// if confirm = true and capture method = automatic, amount_to_capture(if provided) must be equal to amount +/// if capture method = automatic, amount_to_capture(if provided) must be equal to amount #[instrument(skip_all)] pub fn validate_amount_to_capture_in_create_call_request( request: &api_models::payments::PaymentsRequest, ) -> CustomResult<(), errors::ApiErrorResponse> { - if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic - && request.confirm.unwrap_or(false) - { - if let Some((amount_to_capture, amount)) = request.amount_to_capture.zip(request.amount) { - let amount_int: i64 = amount.into(); - utils::when(amount_to_capture != amount_int, || { + if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic { + let total_capturable_amount = request.get_total_capturable_amount(); + if let Some((amount_to_capture, total_capturable_amount)) = + request.amount_to_capture.zip(total_capturable_amount) + { + utils::when(amount_to_capture != total_capturable_amount, || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { - message: "amount_to_capture must be equal to amount when confirm = true and capture_method = automatic".into() + message: "amount_to_capture must be equal to total_capturable_amount when capture_method = automatic".into() })) }) } else { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 3734abfc6ab5..1fff2fce69a0 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -751,7 +751,7 @@ fn get_total_amount_captured( } None => { //Non multiple capture - let amount = request.get_capture_amount(); + let amount = request.get_capture_amount(payment_data); amount_captured.or_else(|| { if router_data_status == enums::AttemptStatus::Charged { amount diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index a42e46ca62d5..b2f73c0b7ce7 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -58,13 +58,12 @@ pub async fn refund_create_core( )?; // Amount is not passed in request refer from payment intent. - amount = req.amount.unwrap_or( - payment_intent - .amount_captured - .ok_or(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable("amount captured is none in a successful payment")?, - ); + amount = req + .amount + .or(payment_intent.amount_captured) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("amount captured is none in a successful payment")?; //[#299]: Can we change the flow based on some workflow idea utils::when(amount <= 0, || { diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index a03e41650408..9fdb96efe55d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -30,9 +30,10 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::RecurringMandatePaymentData, + payments::{PaymentData, RecurringMandatePaymentData}, }, services, + types::storage::payment_attempt::PaymentAttemptExt, utils::OptionExt, }; @@ -544,7 +545,10 @@ pub struct AccessTokenRequestData { } pub trait Capturable { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { None } fn get_surcharge_amount(&self) -> Option { @@ -553,13 +557,13 @@ pub trait Capturable { fn get_tax_on_surcharge_amount(&self) -> Option { None } - fn is_psync(&self) -> bool { - false - } } impl Capturable for PaymentsAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { let final_amount = self .surcharge_details .as_ref() @@ -579,24 +583,44 @@ impl Capturable for PaymentsAuthorizeData { } impl Capturable for PaymentsCaptureData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount_to_capture) } } impl Capturable for CompleteAuthorizeData { - fn get_capture_amount(&self) -> Option { + fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { Some(self.amount) } } impl Capturable for SetupMandateRequestData {} -impl Capturable for PaymentsCancelData {} +impl Capturable for PaymentsCancelData { + fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + // return previously captured amount + payment_data.payment_intent.amount_captured + } +} impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} impl Capturable for PaymentsSyncData { - fn is_psync(&self) -> bool { - true + fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + where + F: Clone, + { + payment_data + .payment_attempt + .amount_to_capture + .or(Some(payment_data.payment_attempt.get_total_amount())) } } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json index b0bc12a6ac89..f621bd52f00d 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario12-BNPL-klarna/Payments - Create/request.json @@ -23,7 +23,7 @@ "confirm": false, "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 6540, + "amount_to_capture": 8000, "customer_id": "StripeCustomer", "email": "guest@example.com", "name": "John Doe", From 35a44ed2533b748e3fabb8a2f8db4fa7e5d3cf7e Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:50:13 +0530 Subject: [PATCH 055/443] fix(core): Fix Default Values Enum FieldType (#2934) --- crates/api_models/src/enums.rs | 4 ++-- crates/router/src/configs/defaults.rs | 14 +++++++------- openapi/openapi_spec.json | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index c4e4aa90c4b8..ffefaa2ad2c4 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -531,8 +531,8 @@ pub enum FieldType { UserCountry { options: Vec }, //for country inside payment method data ex- bank redirect UserCurrency { options: Vec }, UserBillingName, - UserAddressline1, - UserAddressline2, + UserAddressLine1, + UserAddressLine2, UserAddressCity, UserAddressPincode, UserAddressState, diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index a0da9c88ef35..2320eabacdca 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -582,7 +582,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -806,7 +806,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -1238,7 +1238,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -1247,7 +1247,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line2".to_string(), display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressline2, + field_type: enums::FieldType::UserAddressLine2, value: None, } ), @@ -2582,7 +2582,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -3014,7 +3014,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line1".to_string(), display_name: "line1".to_string(), - field_type: enums::FieldType::UserAddressline1, + field_type: enums::FieldType::UserAddressLine1, value: None, } ), @@ -3023,7 +3023,7 @@ impl Default for super::settings::RequiredFields { RequiredFieldInfo { required_field: "billing.address.line2".to_string(), display_name: "line2".to_string(), - field_type: enums::FieldType::UserAddressline2, + field_type: enums::FieldType::UserAddressLine2, value: None, } ), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 056601ac707d..88a0d115ff01 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5372,13 +5372,13 @@ { "type": "string", "enum": [ - "user_addressline1" + "user_address_line1" ] }, { "type": "string", "enum": [ - "user_addressline2" + "user_address_line2" ] }, { From 42eedf3a8c2e62fc22bcead370d129ebaf11a00b Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Thu, 23 Nov 2023 17:22:20 +0530 Subject: [PATCH 056/443] fix(drainer): increase jobs picked only when stream is not empty (#2958) --- config/docker_compose.toml | 2 +- crates/drainer/src/lib.rs | 24 +++++++++++++++++------- crates/drainer/src/utils.rs | 16 ++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a5294546de41..986240f0a36b 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -15,7 +15,7 @@ level = "DEBUG" # What you see in your terminal. [log.telemetry] traces_enabled = false # Whether traces are enabled. -metrics_enabled = false # Whether metrics are enabled. +metrics_enabled = true # Whether metrics are enabled. ignore_errors = false # Whether to ignore errors during traces or metrics pipeline setup. otel_exporter_otlp_endpoint = "https://otel-collector:4317" # Endpoint to send metrics and traces to. use_xray_generator = false diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7ccfd600d662..04dff49b7469 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -23,7 +23,7 @@ pub async fn start_drainer( loop_interval: u32, ) -> errors::DrainerResult<()> { let mut stream_index: u8 = 0; - let mut jobs_picked: u8 = 0; + let jobs_picked = Arc::new(atomic::AtomicU8::new(0)); let mut shutdown_interval = tokio::time::interval(std::time::Duration::from_millis(shutdown_interval.into())); @@ -61,11 +61,11 @@ pub async fn start_drainer( stream_index, max_read_count, active_tasks.clone(), + jobs_picked.clone(), )); - jobs_picked += 1; } - (stream_index, jobs_picked) = utils::increment_stream_index( - (stream_index, jobs_picked), + stream_index = utils::increment_stream_index( + (stream_index, jobs_picked.clone()), number_of_streams, &mut loop_interval, ) @@ -119,13 +119,19 @@ async fn drainer_handler( stream_index: u8, max_read_count: u64, active_tasks: Arc, + jobs_picked: Arc, ) -> errors::DrainerResult<()> { active_tasks.fetch_add(1, atomic::Ordering::Release); let stream_name = utils::get_drainer_stream_name(store.clone(), stream_index); - let drainer_result = - Box::pin(drainer(store.clone(), max_read_count, stream_name.as_str())).await; + let drainer_result = Box::pin(drainer( + store.clone(), + max_read_count, + stream_name.as_str(), + jobs_picked, + )) + .await; if let Err(error) = drainer_result { logger::error!(?error) @@ -145,11 +151,15 @@ async fn drainer( store: Arc, max_read_count: u64, stream_name: &str, + jobs_picked: Arc, ) -> errors::DrainerResult<()> { let stream_read = match utils::read_from_stream(stream_name, max_read_count, store.redis_conn.as_ref()).await { - Ok(result) => result, + Ok(result) => { + jobs_picked.fetch_add(1, atomic::Ordering::SeqCst); + result + } Err(error) => { if let errors::DrainerError::RedisError(redis_err) = error.current_context() { if let redis_interface::errors::RedisError::StreamEmptyOrNotAvailable = diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 5a995652bb11..5abc7e474c25 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{atomic, Arc}, +}; use error_stack::IntoReport; use redis_interface as redis; @@ -127,19 +130,20 @@ pub fn parse_stream_entries<'a>( // Here the output is in the format (stream_index, jobs_picked), // similar to the first argument of the function pub async fn increment_stream_index( - (index, jobs_picked): (u8, u8), + (index, jobs_picked): (u8, Arc), total_streams: u8, interval: &mut tokio::time::Interval, -) -> (u8, u8) { +) -> u8 { if index == total_streams - 1 { interval.tick().await; - match jobs_picked { + match jobs_picked.load(atomic::Ordering::SeqCst) { 0 => metrics::CYCLES_COMPLETED_UNSUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), _ => metrics::CYCLES_COMPLETED_SUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), } - (0, 0) + jobs_picked.store(0, atomic::Ordering::SeqCst); + 0 } else { - (index + 1, jobs_picked) + index + 1 } } From 59ef162219db3e4650dde65710850bc9f3280530 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:22:59 +0530 Subject: [PATCH 057/443] feat(router): allow billing and shipping address update in payments confirm flow (#2963) --- crates/router/src/core/payments/operations/payment_confirm.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 125787e1a30f..33270795b343 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -185,7 +185,7 @@ impl let shipping_address_fut = tokio::spawn( async move { - helpers::create_or_find_address_for_payment_by_request( + helpers::create_or_update_address_for_payment_by_request( store.as_ref(), m_request_shipping.as_ref(), m_payment_intent_shipping_address_id.as_deref(), @@ -213,7 +213,7 @@ impl let billing_address_fut = tokio::spawn( async move { - helpers::create_or_find_address_for_payment_by_request( + helpers::create_or_update_address_for_payment_by_request( store.as_ref(), m_request_billing.as_ref(), m_payment_intent_billing_address_id.as_deref(), From dd3e22a938714f373477e08d1d25e4b84ac796c6 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:33:55 +0530 Subject: [PATCH 058/443] fix(connector): [Prophetpay] Use refund_id as reference_id for Refund (#2966) --- .../src/connector/prophetpay/transformers.rs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index b8cf3e3a1f5b..43816bc2ee52 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -8,6 +8,7 @@ use url::Url; use crate::{ connector::utils::{self, to_connector_meta}, + consts as const_val, core::errors, services, types::{self, api, storage::enums}, @@ -432,7 +433,6 @@ pub struct ProphetpaySyncResponse { pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, - pub response_code: String, } impl @@ -462,7 +462,7 @@ impl Ok(Self { status: enums::AttemptStatus::Failure, response: Err(types::ErrorResponse { - code: item.response.response_code, + code: const_val::NO_ERROR_CODE.to_string(), message: item.response.response_text.clone(), reason: Some(item.response.response_text), status_code: item.http_code, @@ -481,7 +481,6 @@ pub struct ProphetpayVoidResponse { pub response_text: String, #[serde(rename = "transactionID")] pub transaction_id: String, - pub response_code: String, } impl @@ -511,7 +510,7 @@ impl Ok(Self { status: enums::AttemptStatus::VoidFailed, response: Err(types::ErrorResponse { - code: item.response.response_code, + code: const_val::NO_ERROR_CODE.to_string(), message: item.response.response_text.clone(), reason: Some(item.response.response_text), status_code: item.http_code, @@ -576,8 +575,8 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet amount: item.amount.to_owned(), card_token: card_token_data.card_token, profile: auth_data.profile_id, - ref_info: item.router_data.connector_request_reference_id.to_owned(), - inquiry_reference: item.router_data.connector_request_reference_id.clone(), + ref_info: item.router_data.request.refund_id.to_owned(), + inquiry_reference: item.router_data.request.refund_id.clone(), action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), }) } else { @@ -594,8 +593,7 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet pub struct ProphetpayRefundResponse { pub success: bool, pub response_text: String, - pub tran_seq_number: String, - pub response_code: String, + pub tran_seq_number: Option, } impl TryFrom> @@ -609,7 +607,11 @@ impl TryFrom TryFrom> @@ -658,7 +659,7 @@ impl TryFrom Date: Thu, 23 Nov 2023 18:29:14 +0530 Subject: [PATCH 059/443] fix: make drainer sleep on every loop interval instead of cycle end (#2951) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: preetamrevankar <132073736+preetamrevankar@users.noreply.github.com> --- crates/drainer/src/lib.rs | 3 ++- crates/drainer/src/settings.rs | 2 +- crates/drainer/src/utils.rs | 5 ++--- crates/router/src/configs/defaults.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 04dff49b7469..94a29e3b0a04 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -67,9 +67,9 @@ pub async fn start_drainer( stream_index = utils::increment_stream_index( (stream_index, jobs_picked.clone()), number_of_streams, - &mut loop_interval, ) .await; + loop_interval.tick().await; } Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => { logger::info!("Awaiting shutdown!"); @@ -114,6 +114,7 @@ pub async fn redis_error_receiver(rx: oneshot::Receiver<()>, shutdown_channel: m } } +#[router_env::instrument(skip_all)] async fn drainer_handler( store: Arc, stream_index: u8, diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index cc64a99e463c..8101abf5028e 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -79,7 +79,7 @@ impl Default for DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, // in milliseconds - loop_interval: 500, // in milliseconds + loop_interval: 100, // in milliseconds } } } diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 5abc7e474c25..2bd9f092f12c 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -8,12 +8,13 @@ use redis_interface as redis; use crate::{ errors::{self, DrainerError}, - logger, metrics, services, + logger, metrics, services, tracing, }; pub type StreamEntries = Vec<(String, HashMap)>; pub type StreamReadResult = HashMap; +#[router_env::instrument(skip_all)] pub async fn is_stream_available(stream_index: u8, store: Arc) -> bool { let stream_key_flag = get_stream_key_flag(store.clone(), stream_index); @@ -132,10 +133,8 @@ pub fn parse_stream_entries<'a>( pub async fn increment_stream_index( (index, jobs_picked): (u8, Arc), total_streams: u8, - interval: &mut tokio::time::Interval, ) -> u8 { if index == total_streams - 1 { - interval.tick().await; match jobs_picked.load(atomic::Ordering::SeqCst) { 0 => metrics::CYCLES_COMPLETED_UNSUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), _ => metrics::CYCLES_COMPLETED_SUCCESSFULLY.add(&metrics::CONTEXT, 1, &[]), diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 2320eabacdca..2eddaf3084d7 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -99,7 +99,7 @@ impl Default for super::settings::DrainerSettings { num_partitions: 64, max_read_count: 100, shutdown_interval: 1000, - loop_interval: 500, + loop_interval: 100, } } } From 9a3fa00426d74f6d18b3c712b292d98d80d517ba Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:24:23 +0000 Subject: [PATCH 060/443] test(postman): update postman collection files --- postman/collection-json/stripe.postman_collection.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 9bdb5fdb44d9..4d3e548f535f 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -14445,7 +14445,7 @@ "language": "json" } }, - "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", From 394ed908207b8e83839e896e2d1190bd3143f074 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:24:23 +0000 Subject: [PATCH 061/443] chore(version): v1.88.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b86696691e..e427f33e8fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.88.0 (2023-11-23) + +### Features + +- **connector:** [BANKOFAMERICA] Implement Google Pay ([#2940](https://github.com/juspay/hyperswitch/pull/2940)) ([`f91d4ae`](https://github.com/juspay/hyperswitch/commit/f91d4ae11b02def92c1dde743a0c01b5aac5703f)) +- **router:** Allow billing and shipping address update in payments confirm flow ([#2963](https://github.com/juspay/hyperswitch/pull/2963)) ([`59ef162`](https://github.com/juspay/hyperswitch/commit/59ef162219db3e4650dde65710850bc9f3280530)) + +### Bug Fixes + +- **connector:** [Prophetpay] Use refund_id as reference_id for Refund ([#2966](https://github.com/juspay/hyperswitch/pull/2966)) ([`dd3e22a`](https://github.com/juspay/hyperswitch/commit/dd3e22a938714f373477e08d1d25e4b84ac796c6)) +- **core:** Fix Default Values Enum FieldType ([#2934](https://github.com/juspay/hyperswitch/pull/2934)) ([`35a44ed`](https://github.com/juspay/hyperswitch/commit/35a44ed2533b748e3fabb8a2f8db4fa7e5d3cf7e)) +- **drainer:** Increase jobs picked only when stream is not empty ([#2958](https://github.com/juspay/hyperswitch/pull/2958)) ([`42eedf3`](https://github.com/juspay/hyperswitch/commit/42eedf3a8c2e62fc22bcead370d129ebaf11a00b)) +- Amount_captured goes to 0 for 3ds payments ([#2954](https://github.com/juspay/hyperswitch/pull/2954)) ([`75eea7e`](https://github.com/juspay/hyperswitch/commit/75eea7e81787f2e0697b930b82a8188193f8d51f)) +- Make drainer sleep on every loop interval instead of cycle end ([#2951](https://github.com/juspay/hyperswitch/pull/2951)) ([`e8df690`](https://github.com/juspay/hyperswitch/commit/e8df69092f4c6acee58109aaff2a9454fceb571a)) + +### Refactors + +- **connector:** + - [Payeezy] update error message ([#2919](https://github.com/juspay/hyperswitch/pull/2919)) ([`cb65370`](https://github.com/juspay/hyperswitch/commit/cb653706066b889eaa9423a6227ce1df954b4759)) + - [Worldline] change error message from NotSupported to NotImplemented ([#2893](https://github.com/juspay/hyperswitch/pull/2893)) ([`e721b06`](https://github.com/juspay/hyperswitch/commit/e721b06c7077e00458450a4fb98f4497e8227dc6)) + +### Testing + +- **postman:** Update postman collection files ([`9a3fa00`](https://github.com/juspay/hyperswitch/commit/9a3fa00426d74f6d18b3c712b292d98d80d517ba)) + +**Full Changelog:** [`v1.87.0...v1.88.0`](https://github.com/juspay/hyperswitch/compare/v1.87.0...v1.88.0) + +- - - + + ## 1.87.0 (2023-11-22) ### Features From 203bbd73751e1513206e81d7cf920ec263f83c58 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:57:18 +0530 Subject: [PATCH 062/443] =?UTF-8?q?fix(connector):=20[BANKOFAMERICA]=20Add?= =?UTF-8?q?=20status=20VOIDED=20in=20enum=20Bankofameri=E2=80=A6=20(#2969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/router/src/connector/bankofamerica/transformers.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index f6cda8ac23ce..8af7cfd6c45e 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -851,7 +851,7 @@ impl From for enums::RefundStatus { BankofamericaRefundStatus::Succeeded | BankofamericaRefundStatus::Transmitted => { Self::Success } - BankofamericaRefundStatus::Failed => Self::Failure, + BankofamericaRefundStatus::Failed | BankofamericaRefundStatus::Voided => Self::Failure, BankofamericaRefundStatus::Pending => Self::Pending, } } @@ -888,6 +888,7 @@ pub enum BankofamericaRefundStatus { Transmitted, Failed, Pending, + Voided, } #[derive(Debug, Deserialize)] From 5767cecab5c819ca82d97c8b925d8f94c0aa26f5 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:44:39 +0530 Subject: [PATCH 063/443] CI: update actions to latest versions and use bot credentials to generate token (#2957) --- .github/workflows/CI-pr.yml | 52 ++++++++++++------- .github/workflows/CI-push.yml | 20 +++---- .github/workflows/auto-release-tag.yml | 12 ++--- .github/workflows/connector-sanity-tests.yml | 4 +- .../workflows/connector-ui-sanity-tests.yml | 6 +-- .../workflows/conventional-commit-check.yml | 2 +- .github/workflows/create-hotfix-branch.yml | 12 ++++- .github/workflows/create-hotfix-tag.yml | 18 +++++-- .github/workflows/hotfix-pr-check.yml | 2 +- .github/workflows/manual-release.yml | 12 ++--- .github/workflows/migration-check.yaml | 4 +- .../workflows/postman-collection-runner.yml | 6 +-- .github/workflows/pr-title-spell-check.yml | 2 +- .github/workflows/release-new-version.yml | 18 +++++-- .github/workflows/validate-openapi-spec.yml | 20 ++++--- 15 files changed, 119 insertions(+), 71 deletions(-) diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index c79ffa63709a..ecb13f3c1a85 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -41,17 +41,25 @@ jobs: name: Check formatting runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository with token if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Checkout repository for fork if: ${{ github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -71,8 +79,8 @@ jobs: cargo +nightly fmt --all if ! git diff --exit-code --quiet -- crates; then echo "::notice::Formatting check failed" - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add crates git commit --message 'chore: run formatter' git push @@ -91,7 +99,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Fetch base branch" shell: bash @@ -108,12 +116,12 @@ jobs: with: toolchain: 1.65 - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack version: 0.6.5 @@ -280,7 +288,7 @@ jobs: # steps: # - name: Checkout repository - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Run cargo-deny # uses: EmbarkStudios/cargo-deny-action@v1.3.2 @@ -299,17 +307,25 @@ jobs: # - windows-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository for fork if: ${{ (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout repository with token if: ${{ (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: "Fetch base branch" shell: bash @@ -328,16 +344,16 @@ jobs: components: clippy - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack # - name: Install cargo-nextest - # uses: baptiste0928/cargo-install@v2.1.0 + # uses: baptiste0928/cargo-install@v2.2.0 # with: # crate: cargo-nextest - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} @@ -360,8 +376,8 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- Cargo.lock ; then - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add Cargo.lock git commit --message 'chore: update Cargo.lock' git push @@ -516,7 +532,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Spell check uses: crate-ci/typos@master diff --git a/.github/workflows/CI-push.yml b/.github/workflows/CI-push.yml index edc9317e526d..a6a4bde5a5d4 100644 --- a/.github/workflows/CI-push.yml +++ b/.github/workflows/CI-push.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install mold linker uses: rui314/setup-mold@v1 @@ -63,12 +63,12 @@ jobs: with: toolchain: 1.65 - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack version: 0.6.5 @@ -101,7 +101,7 @@ jobs: # steps: # - name: Checkout repository - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Run cargo-deny # uses: EmbarkStudios/cargo-deny-action@v1.3.2 @@ -121,7 +121,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install mold linker uses: rui314/setup-mold@v1 @@ -136,16 +136,16 @@ jobs: components: clippy - name: Install cargo-hack - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cargo-hack # - name: Install cargo-nextest - # uses: baptiste0928/cargo-install@v2.1.0 + # uses: baptiste0928/cargo-install@v2.2.0 # with: # crate: cargo-nextest - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 with: save-if: ${{ github.event_name == 'push' }} @@ -178,7 +178,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Spell check uses: crate-ci/typos@master diff --git a/.github/workflows/auto-release-tag.yml b/.github/workflows/auto-release-tag.yml index 5334c914cda5..4555b68764c1 100644 --- a/.github/workflows/auto-release-tag.yml +++ b/.github/workflows/auto-release-tag.yml @@ -10,18 +10,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWD }} - name: Build and push router Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=router @@ -30,7 +30,7 @@ jobs: tags: juspaydotin/orca:${{ github.ref_name }}, juspaydotin/hyperswitch-router:${{ github.ref_name }} - name: Build and push consumer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=scheduler @@ -40,7 +40,7 @@ jobs: tags: juspaydotin/orca-consumer:${{ github.ref_name }}, juspaydotin/hyperswitch-consumer:${{ github.ref_name }} - name: Build and push producer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=scheduler @@ -50,7 +50,7 @@ jobs: tags: juspaydotin/orca-producer:${{ github.ref_name }}, juspaydotin/hyperswitch-producer:${{ github.ref_name }} - name: Build and push drainer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | BINARY=drainer diff --git a/.github/workflows/connector-sanity-tests.yml b/.github/workflows/connector-sanity-tests.yml index 40a3c3612503..48e6a946a450 100644 --- a/.github/workflows/connector-sanity-tests.yml +++ b/.github/workflows/connector-sanity-tests.yml @@ -79,14 +79,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable 2 weeks ago - - uses: Swatinem/rust-cache@v2.4.0 + - uses: Swatinem/rust-cache@v2.7.0 - name: Decrypt connector auth file env: diff --git a/.github/workflows/connector-ui-sanity-tests.yml b/.github/workflows/connector-ui-sanity-tests.yml index 5db45f2962a5..d4317681a113 100644 --- a/.github/workflows/connector-ui-sanity-tests.yml +++ b/.github/workflows/connector-ui-sanity-tests.yml @@ -82,7 +82,7 @@ jobs: - name: Checkout repository if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Decrypt connector auth file if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} @@ -113,10 +113,10 @@ jobs: toolchain: stable - name: Build and Cache Rust Dependencies - uses: Swatinem/rust-cache@v2.4.0 + uses: Swatinem/rust-cache@v2.7.0 - name: Install Diesel CLI with Postgres Support - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} with: crate: diesel_cli diff --git a/.github/workflows/conventional-commit-check.yml b/.github/workflows/conventional-commit-check.yml index 5fd25e9332d1..ad01642068b5 100644 --- a/.github/workflows/conventional-commit-check.yml +++ b/.github/workflows/conventional-commit-check.yml @@ -45,7 +45,7 @@ jobs: with: toolchain: stable 2 weeks ago - - uses: baptiste0928/cargo-install@v2.1.0 + - uses: baptiste0928/cargo-install@v2.2.0 with: crate: cocogitto diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml index 77a8bad6bc66..6fd2d4947719 100644 --- a/.github/workflows/create-hotfix-branch.yml +++ b/.github/workflows/create-hotfix-branch.yml @@ -8,11 +8,19 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Check if the input is valid tag shell: bash diff --git a/.github/workflows/create-hotfix-tag.yml b/.github/workflows/create-hotfix-tag.yml index 45699bda24dc..e9df004139e0 100644 --- a/.github/workflows/create-hotfix-tag.yml +++ b/.github/workflows/create-hotfix-tag.yml @@ -8,14 +8,22 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Install git-cliff - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: git-cliff version: 1.2.0 @@ -86,8 +94,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' - name: Push created commit and tag shell: bash diff --git a/.github/workflows/hotfix-pr-check.yml b/.github/workflows/hotfix-pr-check.yml index 7a724b602586..e178ba31c1e8 100644 --- a/.github/workflows/hotfix-pr-check.yml +++ b/.github/workflows/hotfix-pr-check.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get hotfix pull request body shell: bash diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index 0b70631e113d..9ae80047a669 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -17,18 +17,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWD }} - name: Build and push router Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -39,7 +39,7 @@ jobs: tags: juspaydotin/orca:${{ github.sha }} - name: Build and push consumer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -50,7 +50,7 @@ jobs: tags: juspaydotin/orca-consumer:${{ github.sha }} - name: Build and push producer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} @@ -61,7 +61,7 @@ jobs: tags: juspaydotin/orca-producer:${{ github.sha }} - name: Build and push drainer Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: build-args: | RUN_ENV=${{ inputs.environment }} diff --git a/.github/workflows/migration-check.yaml b/.github/workflows/migration-check.yaml index 0c4baaa96193..b740bd3a5b77 100644 --- a/.github/workflows/migration-check.yaml +++ b/.github/workflows/migration-check.yaml @@ -40,14 +40,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable 2 weeks ago - - uses: baptiste0928/cargo-install@v2.1.0 + - uses: baptiste0928/cargo-install@v2.2.0 with: crate: diesel_cli features: postgres diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index 3291755b56cf..d5434520715f 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Repository checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Decrypt connector auth file if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} @@ -82,11 +82,11 @@ jobs: - name: Build and Cache Rust Dependencies if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} - uses: Swatinem/rust-cache@v2.4.0 + uses: Swatinem/rust-cache@v2.7.0 - name: Install Diesel CLI with Postgres Support if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: diesel_cli features: postgres diff --git a/.github/workflows/pr-title-spell-check.yml b/.github/workflows/pr-title-spell-check.yml index 6ab6f184739d..03b5a8758870 100644 --- a/.github/workflows/pr-title-spell-check.yml +++ b/.github/workflows/pr-title-spell-check.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Store PR title in a file shell: bash diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index eda2df05153b..b54e240d96fc 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -23,11 +23,19 @@ jobs: runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -35,7 +43,7 @@ jobs: toolchain: stable 2 weeks ago - name: Install cocogitto - uses: baptiste0928/cargo-install@v2.1.0 + uses: baptiste0928/cargo-install@v2.2.0 with: crate: cocogitto version: 5.4.0 @@ -43,8 +51,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' - name: Update Postman collection files from Postman directories shell: bash diff --git a/.github/workflows/validate-openapi-spec.yml b/.github/workflows/validate-openapi-spec.yml index 530c59c9236d..bdb987d625ac 100644 --- a/.github/workflows/validate-openapi-spec.yml +++ b/.github/workflows/validate-openapi-spec.yml @@ -16,24 +16,32 @@ jobs: name: Validate generated OpenAPI spec file runs-on: ubuntu-latest steps: + - name: Generate a token + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + - name: Checkout PR from fork if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Checkout PR with token if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - token: ${{ secrets.AUTO_FILE_UPDATE_PAT }} + token: ${{ steps.generate_token.outputs.token }} - name: Checkout merge group HEAD commit if: ${{ github.event_name == 'merge_group' }} - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.merge_group.head_sha }} @@ -60,8 +68,8 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- openapi/openapi_spec.json ; then - git config --local user.name 'github-actions[bot]' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + git config --local user.name 'hyperswitch-bot[bot]' + git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' git add openapi/openapi_spec.json git commit --message 'docs(openapi): re-generate OpenAPI specification' git push From b2f7dd13925a1429e316cd9eaf0e2d31d46b6d4a Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:46:14 +0530 Subject: [PATCH 064/443] docs: add Rust locker information in architecture doc (#2964) --- docs/architecture.md | 7 +------ docs/imgs/hyperswitch-architecture.png | Bin 1233949 -> 1118587 bytes 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 3ab3b6a7eafa..24b0c726205a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -49,12 +49,7 @@ In addition to the database, Hyperswitch incorporates Redis for two main purpose ## Locker -The application utilizes a Locker, which consists of two distinct services: Temporary Locker and Permanent Locker. These services are responsible for securely storing payment-method information and adhere strictly to **Payment Card Industry Data Security Standard (PCI DSS)** compliance standards, ensuring that all payment-related data is handled and stored securely. - -- **Temporary Locker:** The Temporary Locker service handles the temporary storage of payment-method information. This temporary storage facilitates the smooth processing of transactions and reduces the exposure of sensitive information. -- **Permanent Locker:** The Permanent Locker service is responsible for the long-term storage of payment-method related data. It securely stores card details, such as cardholder information or payment method details, for future reference or recurring payments. - -> Currently, Locker service is not part of open-source +The application utilizes a Rust locker built with a GDPR compliant PII (personal identifiable information) storage. It also uses secure encryption algorithms to be fully compliant with **PCI DSS** (Payment Card Industry Data Security Standard) requirements, this ensures that all payment-related data is handled and stored securely. You can find the source code of locker [here](https://github.com/juspay/hyperswitch-card-vault). ## Monitoring diff --git a/docs/imgs/hyperswitch-architecture.png b/docs/imgs/hyperswitch-architecture.png index 18f42f9a55c50f9b16def5829e9b7a5700d2723b..f73f60f3e35e9bcf6bf09d99d04442220cd92331 100644 GIT binary patch literal 1118587 zcmeGEc|4Tg|38k8ct)$OC`3tQ$(Ef^SyHxS82g@mCp%L~mXId~*;BHQecviemZ)Uk z%7m;FW8da?h8gwt`n_Ja?|+|vx^B0c%XOXWT<1K_W4S+;a|Nj?%bz$(cN7AFoKUzW zqXvP{Uxq*~S&$tDS0a}tOTf<&hg(|C5D3>D+&`k%*ASWDB9XJ2{0&H62g4NjgT(y0 z(sc-=ApF>t(LWGMN(u#;>+0@Av%^G`kn_hOsg~G|N(Mhwe@qW({NIivZvX8L1Oh*P z{p^3g>rdAD-*29mQN_*WzZ@4H@&A`Q;4&Fy6w!aZD;=0}{J-9W+#oUg?>8x!FBAXQ zyKsuXQ{5%b-&Nh?k-rR#mzBS=x<`n=YH5!Se>E^(F#Z~wJwp7ot9x|#3nB1=@fSku z5#ld|z%#^O2(d?qzYt=N4u2s8UNHVbh&@95g%Epm_zNNMg7FtZ>=EKGgupYzUkI^B zi2s)m!h2;NjtlAR}Jh#E}g{vUa7m%oYfT_XNfvUpzlt7P{G@mI<2(c!O> z#S6y&cO{E|VE^ZD|AOH8y*6_-kzNg7Meb>=EMsb@J`o z?C`Zfy~)J-@0zHEfupc5bk(8%9M$;F%f+wbuK7gcm0CT8=51A!oFP<5G1W!x$t{j- z3Xy2yE}NqT;?c(g+U<%8gInwhU{3-s9KMr%B7~Mf;bZ~T9n`qrdrl^%(QxOEYm-CB z$%pT2oTWM^pLpG$*#7@hV0=gBe^nME0;T^)DKr=5!~apx3}Zg@KgyExWk~)<9kpQ) z+5f19uBW2>Up38_nEzK1_8rq}crxUqAZs@`<_62q^f9UK*Fpw^J6n3&!g)qDy?vtx zvxYRs1>07~*INUwrB8b)dKpA-W}hj^%dXYz9%P%Nhw9tM-rZDNn9M@o^+hTmuh~gm z=@`ve%`K85_xfmAip}s~5MqjC6rUCi!+u8d&G|s*k?$6ywsts>b8AdkhGy)~)!3C< zj#bU?YfQd=(Y+70!u`TfeoOdwIc$;F)wNoyIB$h(GvbAp@MFj~yJ zJ&Q8Hi1mc3)a-VaWLt6wYjIXSc29neuYA)LsFAAHtlP>< z*5Q#K|NDmDPB>Pf`7~jy_+Q>S!h*wJf9*lu7-U?k30cqB6idzRwenh@@n_!9#df{u zmY^LKEibD|l$)_Wuj z>w9HzL5iIvW|aKETVr`#qj+Iv!bD$YFVtYW9+=Ipo990q;Prj}qw0es&u=)xwZrl#htS<>R%-nE>7 zHNUp8;?mLyKVG8PI3I)c?0@jDy*_T>CKol^ldCheT8H)7m@Vq00=GVekwu>qbz<3G zHo&?L)cIk{=6Mdc1()G@vp(y?-9m}IKcuy`(Bsr95*IWMOQZLR_$1QrR@;91P- zG?Cawk`!`+q}IQ?yyJ;Mg-g+}f=vVPvm%~LKMh+)yPCI&pZSI1$7Xm|HcByeZ$ARu zG6#tyzhMUXbcvt~3@Tayk(2Xs}~hnm2o4(a2_(FK9d{wu>Jm%(k#4D|TL6)%9HIH}85#O!{SKdy~}99k-9;IPK$1pM^%+DR613s8jL| zcGV9XgJa;<&T#ZR=)lEuXMK{%?+p)lZ=mk)_fi`1JL1*#1{D^47WekHTI8bN27Z4? zd(f73`ta1YA5(Xcu*VMW@#3qG6KbsL=6r$?kY_xZL$ZR_*|DU-LVCB+h3vN!*^ zYo0C?Y`7r|xX$$CF2#>-rGGI=_Lzvf{wl}ft5p43Gl798`jfF+=7dSyXWk*b%4`PV zy|TZOc0UXZnwXlmR?6y>nEbXD8YX8)>!fM`aUO&HM35AEN(yWzn8@u53$*&~Mif*$ zoTM#dlOnX|)$!rW`f3lLA^_Ll-CGv8mX?8mii*U7%pt()m+hCh;hR{9&7ebHW{)VmZo8-)HL zAI^AgGj{E1OkmWLF{#=p6H`$bPBGyQjG?_f&z{iKODu?U{h#qN21;<-4_6AV6!4L4 zL|l}|X-Ho2g<)?si7lWtx62-xUZ}-R4h9Sp6nxR*&(CQ%oed5Qz%X#jeJyb{hLjmQ z7Q*udD0#`10x&2g(5QY^>1d#Q$}FJ%U<^#GAVsV=Qv|{6o6S%yAkeV=Ih5azXI0F?+PD$kl^jDMQ1i9bZ`?6Z>Q-&*? zqr$TV`0hTD))npXsGvx1Gh1DEPX7CZft53f z`d2s~zMuAikr917Z+rZ}@)G?quFNr}T5q6ROqe#?uXeJ4Ck%ZVL>vrfw?*ajcbaLj zWOz-1?@Kil+eVVYzAt{Pi%iwC08TB~g5iv=cew2?qtps|RBC1CDLIp5t-b^)#bU+7 za;}ldWK(=EvH#Q||I5rGNEvxf%1O3tZ0u^u-I4EW7Zv{4kkHew)2tNILqo|`+77xQ>x|B1a5>8k_-i2GamjngYG&Zn&* zMum$edmOvp9=@@!$VM8aQRB&NyB53c_1f^L$9^mP$xj49Vm?**Y_9ms@EhPXB|?Ry zWzh}Gz(()JEnQErRD_4qFYb%6%<&ug3^qgcC~z;oI(U+3aTyN@2#q0-x!+S}CYMek zy%_$s1&kR!F2Js)U;EKxZ>fK`j}7}hCa34^DZ71ZkLgOU57ocgR{(_es)>Zwywu|l zn@&jJq`A$q&uypNR`+=h1K0m$2(SYC$ozgQe7EnRu_`}AC($W;`sH$?@B`$8Wc_Y` zI{`-ugwD>qfdnb}ulE?-htB^1z2tBCe>m7F6G4*d z6?avED{&;~s(!I`bQ8tjL3*ka|IXjf4O0+=*7Nk^=gsgRje8v2JvQ{a(cYQM9fA)1 z33dFmP|oMEBe6 zo$y}!J>l`|%_h&Ch$C>z>J*ILYD8~~Y5*9xE_oen-2Oj@ML1z1pRyMXQH|I4#ua(f z)NGtL&i&w9iscE*8_WJU9LIsFXZWc34@eVyYM&KrI1aZM=P$$pC<_E?9S9VvgIL|; zAoY3U2*co|eupzV`E;$LSX4s&Wz_sZKF|8CPjBohB!`2<;i+siYaMahQXO@sH+i?+-IFJ)p<6UTbP>#r&xLvtQhK@gG9O=w5yK@CBYx z95Zq~-Ab%GbAeO7CE=hdg+It;+v^6Nbo#OB;q#>Mao6?5@we)Y3#jI9wp|!BX zaEbnb)PA$Q6V=Xr=jq{CJy5G~=!3C_ps!yiChm**Y`9E!HHOf3b#y3Ojjb)9roOzs za|EagwP%s1JBd$qlAmJItUY(2%M4`#3;u*M8cL{mNIN|aBhH@hNEFY^oSU1|CpKF} zkE-bC=wxPEB2Gi#jZjI_{L<2uwIOLf7^3rBVB7(Yl6Kx#v+@9T@&PB_xvHw_u?5WF z!Gj0Ddb4i78oRXR^9Pojx{3ewTpY)j^u#1J)N4mJLVXwJV~PplI_GGQ3MabbP?SqFxcYkG%UOUudttPqHw8e$e0oMuH=nYN9*0ZYa=%g1!|8{lE>K)-Lw)XaKgaEswi@VLU0DENhbrtLFRom`Zc$f0(fw!i- zARrZ95W#RH4kuQ3{#O6c1-r9d5FWI7k>de~6?_fIdRxSS>3I#Ch_xH37L7I5Z^byT z{rdj4xS$|HIZ>>8Kr9%p(;cW&?Us}MGPV;SmS#-yq&OojrmCVsGN zs281KlBT@Gi=4Te-ap~7Zqkq05wsZzZkCsn8yE8L*s z`27_hvnag&Nip{@TrB50+`5B{I?ZBye?4YmftxK4qkYT1pgNMSiIZtTJ%*kfqgA6< zP7vYaV?C9cM0Qs8hxxx`o4OTFmr5=?tueNul5hK*K?YZ%rmyr~Uo^}b+g=^T%5iZd z30v-%>= z@|;@)T?d!ko>K9Q4d9UFE|Tqy9kpbr{W3e%)C3hA9OIlJi9U`CRj9*4b)WI~>8 z6PUA}bfu2S1uA)=R}Ur#ic*$Rb>zZr;xq!F6?!_^bcPZ_Dv|g*GDz@QediziXtm%1}}_Q z56n@_ybSFPT6Uiws!%S~4D)IHYQ}&pKRH%25V%lM;=6gO?Hp#P$=u}bNEoG$PJ;!# zw1GGFz?k5^za@-_D)sB!|nYZ|xXV=ei$+;C~lyx(GjKj|X&)*p#~wHIjE|3}W}+_03VbFAU9NbeoOoL=?scWac6v?S#@NiOb)ss$Zl+mneZ2z7|3o%y%v}uYmC5Pkj`4}D z#<%1^-_i7s3MNA5IQCtdS;ke~cxJ=B%%UbX$Kylcaz0YC`L7bb+$ z`*sRul}y|m6B@+MtZY!Ob7U7?G4YX$D#)vJER5~!lMnc4Z}Xmvrl%ikEZ!t$NhLxI z6I!qGyDZnT=<|Nev~lvlB18VLIQO1Ko^@wPsSUgUJ;E+~-V!Ea@x_@n=$GnAH^ERk zXk2*Z^(@DTR5H0nvFt+cegNqv`Bi&ZUJ2R_H;84@ON6YVnN?}{vdDfls)va;Zpm_G z%~wxI59gclWYL!ddpwJUc}M~^D}X|Ga4g0bY7Q=il1@~Jke-kA(1E}4d$EO%^Q zGV8gg1@kj9Sx$<%#~QZ{zixz@1>33$Kkw<55RQ0k3H@QHbNaopOXLJGye@-^t#ege z<=`T8?W3#v9ONR5#L>jOS`CH4Su)|%{+^4d?7&;a51O@E;*Yr4Ier9g=p0+l^! z+0AF^mqmX#o4PU^)AvcXk7FWr+Pgtgb@I#+%&E)SS9x3|+s4iSO9wDaExX?;PjhqY zSm{U|z5}lPrl@KiQ6?h{|SY0-F&xH4>$v1Y2>^GDs>9d%P z*I*Yw@nyVNLI)YBuMqGBd><76$fp>j{f)LCxqJN;&2umAVs~xq8gHeayc2 z(qSCNTcg#Xk@?-PZfW;CnB{!r?mC{K-R+_O^^E4QNtMFv2{=+ojP%=oL=cN_5?}xb zs-!H8xdPW)T{G;utb4}o6s~jy%A~CC*{lU=V$%&RH@X~PT=LPr<`v4)FY%%6GC@6yGY8;-k$u1FEp9jNbDg$+;ediTiz}; zIn&roGI_v8LVgFN3LDU=XQ-@d($^(`x`XE2v+mRX;JCXbZHu^DH_p1!ehGrpQVfhgz z$0V|4MSiZpo(HiRt=^_e#JXA}d0GBJ)9v?Sre&g-znt2Xr!LUv(W_LSD(E8V-JsdD z>T-QI7Pu{3>fGI#E|Q0XhHKs-mIIxqA+?sU1RGdJwRik~_|5`<1P>0=b`nR>rkNf- zN19;=bC{g%FQO(kqx=@s{Ol;&#@6;V#|GKz*Pw^8{u#=}*_o0fX~C5<>6w9HCqh0i zl=RW?HP%L~hC6PyRLjh3$a-_@*Lqj7D(IByR zV{Ex4h_Z~Yywc;1@6xviD z0K5G5>(kgWE&1T-oEf9R;0fbR`D%`^=)lnKVa+K`l2&6c!FBIoTl&F@wTf61vjx#U z`3y?KxTsE|A3tGy(yTJL_yq1~n;9sv&J2oVQ(@h^0Qn(*bItGNu9~O$;&C%+dDGWrR7RN!3kVbwM z8^f)48$fCW+rT-dd)^&#TRhBWl-ENn&O#y^-LQ_7g~mBKYXq%iSTJ1an!PZZEb0F1 zJi#n{{0*Fg-!?rxapR=45A?8oV0m}`2Q~jTfSy%kK2>V4)t3a5v~Ez(Y>YLAJg$!x z={F7zgvqoH&|GL-8}bIg`0%~xPE|NGrVbl^uh#Diitqyb)PMVXaQ)Du5ocW(3_U5w zGO1n-^!Km#*h0tdENrC7XQ3l~Knt%;7ZnjVZ_b?pb~J)sQGCGw6%5`7ishC8p!_6xMEtURdn!|tGxFDh$X z%A$^7g8kztks#_UXJZv)V`Foi*$gUqF+O8*73?jTD8q5KL zirr_<4+qXUe}DJn`m0&6nwI9~lk^2mrMlJbj=w4aiaY{Gj-$67wIoPYCD>FYrM`I| zpeN-2h#E~eQB%>FD-dQvN%?rlIbZOUE@nO-b}N{TYWdhOrZMi z?;O>B6kd=+=6;G3d__jlM$MGnSOUjW-~0ReV$YngzUmJZo`}47Gn=_o9ts_E_Rh77 zx}9L-(J*7k>y`@%Fkio=G;j@A5`}*v~W53-GeKd zVIa=b+S;0;Gw#)G-8Xa6{jbhz-zD+aP)KyW2<8`!x)5?XNu=K?r1SIIRuuqhmA>1a zX%xl{A0cOQ&#;BejEltmr|K+A!oT4DWpXh_YRFuW!xw;^`Ge^=a9bB<=L z#+4K@Re3%+n&9h&-eJ;&N(=leA%6s~?lV7af&7#sN#i!Cl?c42bn%8!5t&MNkV7NC z+$%1G$LWR@5Tog~rH?3JonbxXRzMEr%ieyaR3JISz|%%ys-scRso9TkWavKeaa>hP zrZp>YJuo(I`=4(-ixqIA7_F`9($c-6ll`}YlY76{G@6vD^B$wkPznk%*ZMt?%uUHe z<1^BIG=!E%!`Iut*x19Q;!R0OiJ*I~$H}9O8$s(iFDH!xTxA0gmb!r%@IGE@BjT2)tC4P!P=0fT<8`556 zQNNvj2g1X-&Gxpof?-jzuP(ncU0BBoQpFmH6u}Al+rop1UWx8qKyC%9=exTs5Gx zY`CFXf;Cz+F{v5%vH7}9Sx84ycd9)#(q>d@tZI7H@p;tTT>mzOl4@@G59o zXT(>mfS5Z&WO0vAk)}7~y7S}-{Al53xC$nhxq%WF^hnJTsNy=#9TLVM`tQkeVo|uz zSnSwFb&%QGulM9#pzM@1MwXPEQE_y8-fhxzC%fEhb>2|aeyE(U{8=VfDKv@3LklA* zY9ku5iE4>t>yn5SsN+NO)GU4ZhOaR>zaJ^F>4T#jE6U1ZfR>SonLgagd{l^s`8dtp zl*+bG>12tr7MDpgCS+~i#FkZlekH^p>FrM8tj$^=K}nxYPZJW`S4i48K(ncG67G88 zky*jDr~iDE3+jv&dt0Y4wrwIxw5GkDAbq#C#$9hWCx)9Z%!RVj6J4`<_?}Gk*rXV9 z;VGkxUVSgy^oJR-Xi%p$IhDLKl^oDiDc6EwyrFKktWU$Wv9NYyI(zD=)Rxf<#(igN{W@1}*cv_RuJO9DsEJ4Rd?Y1Dh*^ANZJ(*{ zMfH`5c>9wRP<)aV4rL{fpY0zy+>=3!c+TXe79jH)HnB@@q|R0QQBxv<+4a*T(8=D53TYIu z!Nn~laUKt?i>x$lXWIrf&Tx*I+U8_iA_P}Z)jV=b*=;agnznN@MJ-!R&IW$l%W(|C zd)&mU@>_ozL!5w9w#0cz9>5{w)>`1OEew|_;f?7ekMg4tAedtsC7A?f$Z`$Z$KF9y zDItf;*oacs6I20__Eyn2miVnb^|N4L<7~z3pqQ|03)Q1lZag2=+U2)CS}58+{bHQh z_#oJ9-bClbJ@3&za=*pMv?f9--x54J`1z zn2nQbxvhMMKFRP&f)K^(+N2|;^Fu=Z*)T%p7W*T8+ovt9fG-_0+>^N*s^#0K05Hqt zY3|O}B=tMS(B^<@3KifKtpzSz4%ZE{g6e{yqJMAN=ruGjd-kmOp4rrGw;x364E^W0 zubSmM!hI6}#-bp8!7^s*^Thi_poiD$up22+Cimpk8)`msp##s~-zvzhqS$Q8@{bvV zS$gTo1{qAI z$dVv8!kA?1*>4!z$~8E(XSJnh_=bbZGnVBmJ>4_Nx)B9$+bGybpR(RR<4ra8`n`Hh z-;67m8+rm_N4eiSdiV^1HQNKb^;y5GEi}XaZNw#(hl9VVMO!HMrlyoOcu&C9qkm~b zDbILfw^vCoWC|IS@lYBEyX(Xgl`%ZgDK;D|Hr^~N;C2?_ZZ4Q6X?@`4*0sI4+Ep0h zI^-(B)!-$Q!F3p6aqnbg?f`Ci7B;cv7|8g3p#l}i2&UC2eESry9tK`sxt#ejO^oY1mGRynS862n!&Ht7WgfKOOUd{IL(3ac&MuiD zDP7DheAR(H-9ajiV(pty$I+ZB?y|D7E{+c~SuM31=Gcz6HGN2vL7@}Djb0!;y5bm{ zGc`$G&k>BqQZjNTky5Y_j4Taw^mC>W^W~L|8RQ#FS<|d=k^Y>9(aRm4>&6CCoX=N^ zlDBhWM6?3e28)~D7NY&|GS0bK%iqeh_s6F+BjV@g*bO9BY^6eK?JYSKO@#_L$|oFV{b{ zsiyDu2>Nl{HNF><;Un1NmNRL&+U>1-Dm?tp+`ZdjQXX2DBgY9We6HsCbiY!GW zffutPqK2^N3}|VM;Q!3OH!jG=;v1=L_RqCsEH!ImA(=tIXua}yusq$p1T=!k$B?p7 zU-6ptiTKQ*UA2!3f{)+|w5L8qT+GZ1G^oLJh+8r;h`C(e-sr8%D`BA2ta>n~{lZPD zOhsmJAaK@|9l?np$Gy!EfzLhdr%P(+PqXiaDdCOx*i?mP`Bg-9HCPB5U)q4AwXz`R zVIeFpP_A*j+JdU~D5jh%P2h;(N0Y7TA1#r<4SchRqP?WL(xYR4BE53#s`1FG)LwOk zAtTU4pBnb-xbB|(H=2|Mrx+1kRj*SUDI^l57l^; z-3`pnHH(uk&@J$`{V>SE{pNH*f!l}&+m-mhw}KuWh4X}kCg0~({bRBA8_eay0Guk5~oE;WB9ItI0G)J4=io2V?NhA zO+M&pqYO*LC7Jx0+_I6|*aapmA1q+(R9RzTDW##c@xa;u01u>tET#8GG}b*wG?cby z&86sN#y6-2r$U8C+^Z!T=e{;A`onr%dsmBzD*DP67u5CB{dOk(l04&ZivpJA^Zt5V zHA=yy_+ckRHIh32IfWW`)Y++E5ahuUj3h64`Vo%t=bHX617xe11D8V#wv4l2>WS;| zLMKnREr9aw`}w+*`kWzyV|n=d#r}h>faHKlj?4DS*C3La;wVRQ@fv z!Iv~t!0%2H%Qdzxf)x7RfiZoNqUQfE^AD(OL)6%UeeL@FEL?19>OHws!Ws^WyvI8Fo-9i?QdPZcO*<@7Hb1)q70=bn#f?5TibPO&V2ao0HEMG{KesD=84m( z8I>cxn{%zHIWIZB7i4Q9)2rOj>8!>cSxr5`%%_%lu!}s=Y4??6W%8yS5u~(C45k zV{fPsOuD5i`=eV*#`;>3lNv$JvMD)1mNApS0l=}#Ej=r`+TfqiE;zJn`3p3Ki`jv) z+?u0DHuAY{x3pH*c+!1r$Sti%LtF(CIJo!;g-nL`L8VqDaTGzIaCevHYF0Go_6_gwl#k-?Uz2ao zZoy<9ZhdihaX=(V-*f6UK7N~hV?UZ5eHmBKl7j#_zB)h{`ypB>@+v}XiwC?W2;^2iEOY?4ya<`%@;N0UDpc z4_cabL*)_E!Kh3Hl02?4Yut1|UDI!y1S)n=9?x+_Gq{pNPCsm?RQR!&--VT!A0sth zp)WOU8=`JRs;c`VrdcaJwvBvb?IFse9*m*Zz)0#L_CIX;P|Ny}!d#6E$54N@hQ7?tm(QYl5G9 z7G0)xDP4T}vNhGRRi)p@Y_jK}T`nHGN(9dj%fG=IXVV8z#0O%}xbkB4tq!Sq328}# z=V%&5wGP~E% zFH7#){@c4+AJnXWBor;hrtVc50J#-yblWk0p0nn>P`O`$$%}k(_!TqB1F%*CxvXfZ zc~PZ9cZ;6oc7HbQKglT3*ZSb7(jiNwM~)%0^TSn{L9+d%dlNBC`aKa4l??&mMQ>ab zSK}t1j7AuO!2Rt-te-O-Kw?1P4&@^;m!F?L4bEssiKs`Kg^2WXl|j28BQLEAs|%RO z2V?f0fiLXi^v>hEYCYRp{grMtAw-;_w>m$(*<-6@0t=yds{Qp-Z~k0y`wN*LIL8pj zGk}`Ei^}yI4N>Dn%_#F->kCs}7x$B7Qe9?%JO8uS7E)gg`}zwSPji8yo5fp@oW`{_ z-ipdp-2Im!J_Pbd5&XOJDDV`hc1&Ho8$5Y*%(HuvFi~k>D)A4%?h}Ig^@>rXXN5tu z1PE>!!}$kZb$depcfPL%U?0~%YKW!nfHf+ECU-`=YC z)_ASrLrtoq`+S6}2;{rGUkR-e7IE5Se0;F=ZQNquV8)`a{_nM&s-XhW09B@(jky-C7UOTA&#>7>QY4{jR)SGP{#bj(jnODnYH z*E=FpayVJ}Mv9Gv_gqP@J|`(D>67I%g-O0!D5l*fz$U3-Wjv4c!G{d1y&@MpO>ndP z=0b-ZtfS|k$5!l1W4;pAW$Wk^O=}VxJt}tVWe}t)oU`)I?N*2_>oWbY6Xi3-BD07a z382#8z5rTl+Vjuv)>t__P~05!`+~VKvDXMvFXRDS&o&C|Z1Q$dVW__Q_a|SbBUL2( z2fk_dKKHnjGRx9FXt^+0w>@Y^c0qLSDU1CSS&p6^zrMYIB>Qf8bpqqE{mw%32UntA z{Ne$M2(x&eX7XJ#>X-9dMehvrHz<(FoCs>Uq+FWISJ7o*@rx%yVz1kjeXE_XA$Y52 z)dO7q=91Y9sq8(pxV%yYwubt`~5i+Dd_S>20ML z-I@?SHnRXWLXwXp@AVD`q2|4_dTk%D8R%*qPKzod?Gm_pNv2&ND)f@-?$_FtHiE5w z;+LmEl89;NI}-wm+og7v`EALCQo-_`l3R*)(L_18&4a)0++2ABMPA(NStN8Hc80c| zXqoxATYue^E!U#0kJr4A{3|2ws8Id};kxyY(K69HQ&QV+#=)M#Rr&B-DqXLs*D<%V z8(K#Eu-kkpeyt&>5exnBnD>+}LX6yux=v$(=)e$%0#s9a|F1FD*p0c*2B zo6Qgs>~!TG9pFX#r~sC|K>ys$es2ttsD%Q%a&HOa5g*@yeQYT7z27|fiha$p#@Uxm znFva`+lK6Ki|8s_QJl!?FB07GhO&{hbJ^3 zwGtz2U7_hJNzHnPj5D>+vT*+DND=qo=*zctTr-w~-aXe>l5?8veKGNPe(88gFp7qc zDo(Zv|G5{e%O#?^C9fC!FG0BM7AYLsdx&v zsS^3?Ua0%>5eKEnK8=8CgNfIz#pH2M%{IP0CYLiRmQ7FXFfi3Jlcdhd4#>-Xc6CN# zuuBX2*e~eWiHNytb@8e5^25m`4I6u8{Z_o6tLd?lkYQD9Qg8wV9d}90p;TXj8eFW@ znf1>ngcr#mIhvu7{o^%GUZxpS8LGAp4B@g49N>!?hHBrZaww1lAg4@m>zK z_fD$@(F8O4nw^cA8iT6Yz}`{?2CogLo_m#Hs59it-z;7VPGKDl;ar&^?jDROZ?wkk>S#6|3F~jmfs5& z+&z3;e!u&{k%zYTUU4}VHk1dfv0;BCCk()Pfa=!?3kxgdP+NIR%JhucZR<5xxT~$- zONYHNCtM=zW2%2KE{8%1pJl2O73I}?DRyM~2h*)dCa^Djp5~j4LJsduCLjlwfv0+T zkc+5^p?9OBi~mwE;{3pHm0R07X0lD|`M3|j)+0!_JR(D=lWmIr#h?1Lu{8Vyw(65R z5_Ji_{1n86$HBlj;|ndga^mtybu97KUpTji5%m7Pq#Q(dR*W6V^!QRF%b!Cto!q(>QV6(Nk@H@_^u&^>{9|N?V&lShl zj%~;Fb1zaYzdObg<+nzTHR>N7<%$0Vc+EuM1TbJD1HI6myJIk|4)1=k(mF0|&eK_8 zf_+Z-M;xzl42hk(7Un~n8(R%yWrC@?TQ1fs;L<&zO)Xi9@6+vEh#=-4b11a7e{U>y z_`;I6^q6TY1*e#;kEzxy_vnm=W7ooCsMxak=SDY2R$J-qtM4MNbj3ruaZ4@4jzLVp z8YPtfJ)`l?!b8Qeu;1#HP28Z%IeWJ%n_7h!X|0{)253O5KFB(J_F`aQcs8clhJg2h zz4^R!2tM+-rf1gBE^(O&&aSA8p{yHbalJOAK_6TD{`BTV`hC=jt~l*HUUG zzK5^i(EW-+q4beJiSS6f+b5S|ZuRHtlpU>Ch@j|W=F11^)fiBw;{?LqJjY{r47hPD zd2<}bu|@`I>)zh1%cf_h5C3=EE0th!zP-Aw&Tf>|fTVhR`@H4n^T1xz2+&Q|ZI@w7 zy=Qbr>3r&}j<|JuoLTf8mhR1BHVl~#XbtEv*PIYVeJJN2jq~HnF`{z41GBNX%rkSL zE_vH*Yqe_EX9lb%^09WY!mk3aN^(BGq7T!=ZC<{=GY{;&nqefjUZud%hszNn9l*Q2 zZE)5;0Bk7_-65J)bwiI_q2k5H3BL#T@ z^yK8^ce4FEPo6y4Y8h=7>8x!4_I2=pV(5P$>FYn!!td{FflqntsJ*J*^vOG+7#tf! z*hZAEtdb!N*C8cxt$>XOl!Ffz9}fV9PrKmx1GC z)=c)@n6hBR8QafGlbeolBZ^*Qs$IqgI99i?hz{&~t_sc0iEKB}Gg$W>sBB#_&A=pH ze{LfH>9OhXD8VNcIdNaMO8Jq0IS!G2&JQH%2&l!1vDmnN>Fu^uCw9O=GJM;Oes=so z=zqweewhVLvt&4fr0eJ2D~uBEymXLKXee*#hXUT_hFZtMg;AyQf@OD45JW!&;ZoVz zLH0td5^^@T*OrNTkKsE4*cXvVlu= zC_mS3xDu*84(p>kGt-J@WMB~3#%-|XGN?JM$~5V?ui3vXt*@cN+ukJmYHMG^Zej3p z_=gi9SJ5xhj=0soJkuNRJNfHZKEx&sg+fi3sE0iirQx0iMPg?5_Q=&4CLG9-a90IE zm^hacDU!nM+C9Ri_~0Jc4+WOkYaE0zxw3g(s&Po2FhSc6{5N^vCqA6`;GE_tO#gHp zWMH*{g}NF%z7)}W=}>W1l@3kB0>im;(Ksd#cy{%Dd9_Lai9bKfA%Pz?Z$kZEY}DSo zg4*6_3c0Ujjj(NsOy!=;@t%H~OhLF=Z`YvHD63UktJvx~S2?MrJ% zKiL{?G2E3(3!U*rWVJb*-8^j*Y3I*n(2K4LNA;3UTN5}GhQxhf<<#X6@UexQYM=?t zw5ZtV=`Q2kB!V+&3)D#!;Zs}IU$<4FP}qt^aiCJ#M}0t5-%%Toeva{!*Q1I7ab`O$ z1Tb;}Pkih~Ylj(bWKjjSe4?Y8Zrx%diAqHRSXnfFjYg{pe5uOnUIGhRkn!ev;^=y! zeqH>ygM-7bCzJejWp=|>`Wor>FE#NNs&5Z>ub3Yy+Xqg0IqA#SCrE z3?COTJ<@W%=$w+6-Eze8b6df#x(_QaY)dYkk6CT4?O#j+AB13FFs$;xOjuOS za5kqhgl}ZrEmZPGJW|^DtrKrNvr$XQp$q>vi14|Td~Tddbm+OHjKKQwhj)V%zc`%+ z-+XP7-_H6X&erZqbiLLJ_=?3Th(*agyoC2yY^9^@1Ws-jH#KcExm8kqxy8V8{gvz^ zx`@$EcmCR1$JhQM+BYtGgr_b>k(*7=x0hJj3ay4&p9avx z2zDS)4dwH-wI5dWo4bc6XwUYRekgsu-m+j7;jFys-BnoYR6FIR%T$O|7MF7?>2RAs zqAOL@b7e#O&0s9)w96tfj?35N%zcXI(LRU;f!wxQ`^$V?QO-;0l6T%J@H28Db@6Z= z0_n>EiU56ws6b-11nFa|3d`95Du}eX#rW(){HLtoMf+R8VA(_d)n4xxEQK$T)?R*? zLvzI_g6^{diAn$4S9I)C^i>0cgPd0aniP7@DP*+?I>x1b&(EW~Ku?bN;d8&SO`9bn z%C(FuE}f25C%wU5W~*5F-pTE{z=5LP6USo_cOs2hpqy)9`=H9kfq&9j){!=0YMc8ex`gBEq3~EDnRak&WCC~x^(1?DNObk2T`$&7Y<~H+)7Em zME5l-AZUK1X7E(9l}~v4d-qdzpB?Wf0>q8zId?dLbrbV)b4j4Tj@nCm;sJk) zSm``dC(+x%IbiQOFZIpHHv}fmv z80S*-Wg~>_DCS2c^J`6{e&RaD^-6QQ{fNnklfP}x2}JnQ=E~7tq~89XYglr_`$a5k zXCz!JfbN7BF7S2?d_C;7qt9XsTbhX0opzfSWh0)}OTKskDjpi~MnV$|$$J#O$-Y|Ipzj8Q7KF}c!=K-kuddw+GK zqe8wAws-Z(5z7+X@x^pJzOqpN%=*K6rYqA&^GL0({Gf4nwH2>3y)}%gD3f6_En)np zxNzgomrV9q(E^bwr!F+h4TP%Jj$h4|otr!GLE1Lab#+l9TJKD8-Y=dZ4A1N%?A#`M z{v`V6A&9r$xklL@5lH_l8mHS|T)O+*gD)K<=n%C54GP+6_pcO0S@I`l2M6U`6c85R zEzJ|rDruz{;z+yK&(>4JV|QalBB@xqA8Y~crD4x199=!)XsaD6l6y5P%ICqz0^4M( z)SkxP;pY1j+C&e7VDajQfYy$Zdm@uChShDvWzuPXQWj7HMfwe0Zn47t%2H&i(tk9T7j zWc3*f$^O^Jd9W{*4VXw|U8|Wvjyz zt>^`cmOPd16VX}`UveTZZ0bKUkLwgWF4~jmzs<-Rclvz%3jV7}koxIMB&DIiF6!YF zJ3d)a@38Oe9v)BsVJC4CQ*u~@TeqBd**r*_MfnReQK)|<|5pdqcMNB>P0R(GT!5pI ziH=USmtkp}XbPo&jYNnjq+h%c0c~+=U&F+Xf1z@i&JL@~E2@1M@2J&rWuY8cgWh!PkRwS7&8#Qt6;?s5&w;|W9o8b{!Lcn`S+`!Z} z2W{+NNCh+QY;s@+JfWJ>9QIb49|iS zXPB_C+!0aCLT&VCZ6O}mTeSZB0$>bmskitl9QLX}g4ED+)V`dD@Lr6g-9D&T`y8SD zv3|c@;F~tcMIld^R5oV!6~chhia8fqRP=)_9%dh4`A&LfvqJdGR;44|v5QRw4m<@C z0}Xc8YO0k^bX}5a{OFZ-{79B;uN*A2FarP0qG(|pZ7xWmJ4ATG>Gi8gwKfAom`;NS z(*~9c3H<<8A6E%BhKfRmf=p=K)laK&iwH&S;ko?Vcadt}^0D+gH5P1UNZ$B=RB76M zqE0HRovzmdt=blJ?Im$eVaWC%hi;!Df=GS?J+u__&ynn&Yc`6grbGg-L__d^e2u(o z)jiiZ8e}e@+AkXM$@|b1mVKS8%5j@%zz+d4`&iO!uXl6AZ6{{Lh&894>%oGEUO)bB zVbvDs%KbY>9X-LjR*XlIf)XdmSW}}eI&ho`?kiD zN;Mz$Z!9-!l#vaNMM@vRKB7+N>%L7eAI{Ft={R`d$x|oNsb;b0Hb+aWbL@3;X{(ac zB79L)lj;8q*LEDu@U=`HRyU`4!Y?KxuFM0XDHFv5V5h#X_o}NzuOPZtjiX{_p#wXR;ywq4)TcVzuGqKCi&1@(1$Y*=AG5oFh1z9>mUD z2_lt)R6Fez7tH&udP)Y~%e1Zzr~GrGxp_;|+7v)KsC~WzD_SoRdzty0X;2q$%7TsB zhHV6S>J943Nziml{kXt`xrZ#H;2#DbJAI(kWK^iC4J(Myqd69nB)cpZo=bacV5RZdI90SL#WX zz1+4?;suCGGAOnrAFQ4pHkX~BE|C&|rSW+sa%YT{B~w2TEzU$zC{KLdtWBO_MV?{o z%myZ8ajqOLjY%oyDR9D^d2maBVXdly0WEmIG~)XlGVwhoSrw#~c#kISljJ_m%6@m1 z*Swu(SZ1Gu&Vy|c=N?Gm8@dSsJcO^;*A8T69191M`%mwE{@^1q5t>%au;B1#{dZjX zUxi_w_>sM(ucs?vo{#+Z#G7A@_6+(e>|bTPRclG7>$QknY-dq-L7^~ba2&)d}Y->rx4w!HfggiQxvsAVGR3KPP@lG)Qw zj;+mrOIyVkzZB}1=jTe&L?UCWH94;r-q9h5yp%e2Cng1eFN$YdpeAFHRqpmq*1d;k zya4#^lj&FC{M(}0UmjyVo)gTznx^yo`J|6yP$9b&zEXOYGkZ3yx=@1J@!7O(u^fQc zdpjKhPO4~NR!R=NE!B4+DeX(-IZk|np=gEw`|rOA#P;QK9OnCZ8m$<1zJvD|A}qpq z@d^KqrSVhw^Y107pg4smPiuOvqs=;m;*>aM zk|F&INaP3vsa;O9M}6OhI?s&4O@qJ|i#FF}ou#Miv0H3|0a4Kv`mPH^Eop6H-#>|2 zmrKNM=1YPI}+OeyTT<-@LV6#$N%NiE1m~J^<)hTw~WOYUccKEzpoAkdb`lfb1 zNxAhCE^$Pfv5UE&M*LO1Ex80Wu>&&}#FMbzc4#$kymuQ2aEY#BC-LL?{6?SV6M+Nm zpW++oP8K*RIMZYVqc$buESh}Dl@=5ak4HXi_KN|Z3PZj%mrk)lbzoCm$YUnRWHg1~ zz`>JX`jls*Ks?H65yY&P!#&Kc8Gr&eQedwl9nfb?Fr!wHx9u0#asVN7>qSs<*Os*@ zMk%VW>T5vMSNwA!8{U06+5WnM3%Mgt)w2KamkRTae{Q*O6iYyNT7gKzQvHu1*oL9B4h@{xta*b?ZlqLN{EN zLchbC!a(+t;FYELr|w~BIL~}oTKi)s5;eS*c5*NX-!yGKx5uz->PMGfG}c=1Wpf0v zk)=%59wy|*hOP4Eje>;HS1yo_(TlJUR(j;2EhHF}E}n(yss&V30TptsPi%WifR%Uq zaDhui>jcO(A{AcU5;MGqKq;;ccEu9&p$#qx&E@kT5(7UleBkdTcBzw%{;msje`0lV z7IkaA=!fQ|Bpc80ba3in!M;Eg4i8m{Ctfr;=W250tiI z7Mh~Pd!#$W5J>?rTjk3g?GdpN+Zxta<=2Pb9)bXL5-?s}nH3P*?R*Z4hU+{O-V}|A z?kpAl6tg(K7xL2p8UBMNIz^U>|M zY+LBy&m)Qm^AS$=1Zd?8yX|=1zR=WVmRj%@Nf`os68&8A^DKB}O`Tz8{{H*ja@oWz zSI#SQF&|q&985drP)~o|r-;bLNB7M-fA~9IS?HCP6qZxXnOaE9m@J4Q-AJ53CfNm0 z-sYK$!m5n5&&@0f@5zIcWW@HzBl8D5ZQ`x&*YF6)zxbUNLD$X}%Q^|E4fQ_m_P{p!ru3!pv($fj3L z_!Z$ZN)r{Lb<{f@K@#-pLYK~fZ6D=cO$LND7+IUDNfwSr&3yH26$S_11E{$;eoK7N z(2PYyYGIB`7Z=F8gE9ddze?4P&Of#hgtX2;htOOA`{+!Q)8+TqV9p*wQ9RH=Gvhm7 zg+J7#i5NbLJ{y4+-X|@|f2ke53#&b!S2^{xJco~G?Bq9~3axPlnh2AF&M)~d8J!sq zBYS;&>f1k}oja!Jm{^{1(=CDfZGBP7p&p;06xG&VAxiUnz{Piu%tOuHg64Z9a9DI{ zvgKxYUx`yzxlPA=a5_x90>d84gZN0_$H%+~3;8o05wX5(!<4CM-R3{m1+uaMfiQMI z{**X{`y!+J{3}51!TCgVynqNk?TP2LPU14u`^(_eA@P`Jd|cSb=N{|0yuP$b&;rRk#dYa*Y7MOuhm*K$qV|3$ z2(SNHcr=s%1aE64?kB4F_a(9Y&5vq7XueWPSI!j(b0`Sag}$vlA<5+fGWl^IbfJ8s zeBvY-`vB@)yHe=CS4*_sA5RX>?$Md2pm&RtJToTqUeyNM!Ff&cu0XXKmadyGnRG>wQh$qh>^CsodLV$ zojnJ9Hm;TcvK_@jAu+&d&NRA`Eo=B_>Or7{gGqtIBR+XI8Aj82T7aN~OW01r>oaFb zm=U{h&2Z^t2N?ZIL}9i@ijMWbZj*QRih_+jhM8OI9bQqAxz)mx%PhX4vU^-2+k2qj z8*Y4}?tg@ghj&4bG)_5@@z&0t3k@%YVmq~>+<9z-;LJRm$0Pmlh={YKv4xy9q#A4R zkm_sePDO-gzNcAL#|hnc+^{cbW4*!jW@X{tn9jx>4VQbMHi~r?hi%C@CVTE&y(#j% zO>FtH$Y*q?o44)p)Kt|yuf`;5>e7bFW02)J>gG0N6YAFIOPU$ZX5OD>^5^gJ=8y3K zd;4vUYD}|p(pUQys3L+R*1*z;6g&51bn@xKn9RIYC&l?Y!utEJqIN&*Pl=><#Np4` zo-JlYq~+|p8*6X1CvaaHJ$J(4=Wl+w9+t-ehaK-Zr>Enr^YAVj)^k=*|Hu;IzTAtU z$B99MKgZY`V|RMsdSB_RqL_Utb!)CegrjwAw!3})&~m$c*FWa>dblRBSBq6GRM`+m zqvIEKw}1%b&Gf6aKKud#TnaEztvatK)rD&Y`ua@nUuXOS@nA&xP0AEtOR)*$#G0HD zBNB-fHdU@yAL%rti(KqDdt0}M>)NST7GE!=9d*3qvh&0(ylc2AvT#d1CTP6dA$z9O zmH2OEtD5^tGjDTwQ0in+J05HKF1qIj&p{I@tDao<;bnghkMlo6qjZEK``Zj8V-+`W zR$d)L-BPzcre7ay{eP3=jf66JA>SEKvlPq;-GZ8ojw6R+;OvS;vhTd0A|VBXftIftpb(WH?7NaMmJ=B%2}o;_=cl=yaPB|rG- z_aQ8@zx1I)L^`fDx8gWGIcBO*`y8EpPP)Hiw2|gr-ZL;TIk$vCZcj1+wCvV@zuYvV z%gQ6K57>4i1kgxuSF(23-u01)KPw1(-ere;169|yx*4(G|%T=--1Q*a)+h5hBwSeH}`~z7L|ge$;h&- zdo3;0u+{vOwTE0=6MW3<|HWD&i$9I}D2AQ#ZxufQgGBcmv^eY0qc#BG(h_8W?kzA4lj@X+6%tYQ$RzNuB{AE|un4j)Fq%1H^3UTt#~;VVUAxq~ys% zv}cplP>J`cM7~?nhUv?V10d%QFecn5Hb6M6xeQ&$6LjR<{Fj-qoM`7|JGa}x>dh~5c;zt1$4Ew!1(BRR~`_9Qa&&R{L!UnNt-kl1IE$ki`un|`>E{-?Qsy4W?|T?*4Fyz?9%jA!fu$w1z+suWjv%*(#CJy_MMs&DG%db6*QWLRmrr39__ z$&i}xmJZ)Hj!6PuyY5!*b}T_t_T`-Ypd2LJqZfr^)e!S!H0eT|s1X++XcZ^7cILXb^9s z9+DOWd?dWtjLMe6^5Z>q&nMs)09;~ZrF%=kQqDbn$J>akPo?9)+ZFN!><;b$L6MuX zX-BHiaM>en80x*6b|Ko0>&r)|7}ctzxfAwk_YbvpPFn*7{;wT5r_=voU7!SIS!Fg~ zQ_svzr@T;Pw!>@ZZP#cQ2#=S+vtTQOGQ-cMe`@Xx4+DFlg2(@_xVHiAyUz`r*S;ph zQQ2GKX?N{mx;82_)P*ZXT<@!}#`}{o{=B@QSBrqj(NfeZbgOJ92f@BiwDjWpW8_L{ z`yerOIY~of%JGEA*4MK7mls+Q3w;BPQ?}-LDO?GmhkA6|4jQ+)ip-o)wc){6*qgb5u6D})dpkWPv%tXgnDRcDSY@| zuEm|v6R$*MI^u@9d>IJ~=FV4&?qa%nKKh3kym~twVD*fST#)UDHZTDVw~;f%W4*=J*PmukS1W+Uzh&_7gckdB!{xZLh2>cf?Sx zY3@(WVYW_sn){DKUo5}b(++j>xDsu>+KfpB?ilg25jP9!CmXkq0e93AYH2=yqg6;M zrC>lR0ZNFkKfZ6lH@~-{>_N8$!X5?eEPWRDw&M@Np)TwL}b8V1O8#&-@ zahEs5MdjPy93PPy)n5Eu%_bb8fBbD6YASN?OEoq-#CG9%e zbV~JY(JLwZ6w!Q)_UBSvy}?S|dpHQ&dxFA7JZI<~P=}gqe_zwlEE}5w>aW}+o<3S= zq^CG!ir{@cJaXe^9=$Y4skX8%hFRQ3(yk2q;|vI!XZkgU<%uyJAsrt*%xJWGp%q0( zx=Ti=A*s}9Ual*bBdwB@iLNB7;)sjILoDAx8hhQ6_J6#@(e=F(G1`BhAEfDYL4xDF z9PH2&EsC%Pg?}@gcH!_<+O87BSWqXdhw)LTrAT+g(Gs}o^wFDvbyGf;LU7nEyp~Xu zhW(1$wF8n%mD$wrL)7?=0rNrrr^rg!)htjH7ON$!BtN*n1UsQhd;t0gxsUVvT0}CE z4=mDApJYR})H*-J(r11FaH3{S{b6+b}6KrWFA3fNQd z<()i=uW5c(lL;QZX|xw2>~bXDIql3&dY0V zDjsh0y2p|>-*S{_TGC22{rNr#G&;_<7gv5e{@qhvyQcgT!_W z#b+}-N>J_F%F_5T4wIZq!^h~OM>S#+aOG#`qqGY3sUK(bT)GfE)NcVz)|k?j;szo( zU}JQU_s<(|-ZaNwJVr6$6n}vBeAHgWaQskvr=Il$Ywx!H8@pko-$&VIa{Nbge6E}9 zXZ-bN&qITpAE%29d-?g9akcZlQ8bXsL}#$qU@CUuIeV*6s8@MU(DvliR0C4pk(pRz zY5j`qg_pd`by5hPmNe`={RN>-srsg8oXpu_oevk{BE=Ci6<-%(=Z?{q`9**!f42IWU1BJP;2ts`A)lxym|$TqDeqz9BU#Q61(8u7tciw##cB5y9|2O1}87z zT2qkSRyRnWMq0YI?>89h-5@9mo?yxCiacP5K+amEc-SftryMaDX)bc}rY z=&Dpod=1~a?`jetJH~bXc}7Bp==Ic9xZ1CE%+>BYVZNYdI?O(Vnmvn>nenypABx+I z?f-Y%-`Xv?JArj=^TYf$565Hst_(8*h>TRqWTS<-9{shW=SAq)m>ht4#k)!#N-Ujt zGIx+HVg9slMPa}fPHqo+Bl5i%HdYR|tUIBKit(@i_@uLxGuyH+<76MD)iU$Qtrz-~ zB5b%n^_KCs@IFfQ^NC^*&pzIrF?561p6uI+GVM#j_ZN?ztOkH-+(+|>Z8_j#u>)+^ zmi>nKiMn$wX9XLV7fF&;%B8!`7LQKZlH)GuE{~FM?cD63;=u#UO@V8#%iai zo44tZpX16(rA`85|F2hv)>>(2h%2D{sNTIEe$@ahq*ER|>NfZ8-K($$G!e%@O>L%* zD%)%r`U8Na?Z{OrZOPO1#Q*9uwf1w&w~!ATQ3%-y(Lpz*T|1FJK5aB4XsNUnqM))A z%w7VBWyLRVM;NXSPpG}EO`Q#apGzJGFj25?Eq{iUV>k7e*b)$X;3Jts;<6k24o{=--82v)>HbbG&r5ir&MaDo>ULbNuHU>2W^!U)gSLH zvH=byAAiz53Bjnx1`iVd^j}s2hi1O@G1hKwf>1us^@i-X8UDLmPId)aT|nhQ%%*BG z+RW*lef9kBIPGUxdeDNW^0{^Dq3Tj)|LNzyezLkK;1960-1C0>z~x~Ir1dW}_(2gv zb}`e1Nd}0mNGhjLOFOq-`9C%VfLf(b_1&ckfjCGLbrNQ@N##^dKm!g!rTq4)8VnkT4EJbnHgs4q8PsE z@ILX4x`F>*!2FBti_$5nLo7MX`i|Lp(-_+cufx+j`;wx|ewJ(|!{#hPXn%@@+`x5Y zPhxvggzsVaBJ-##1c|OR+NAI?$OY@g%WHQCk=sxK&dG>n7x4Gbvyb;36-4nf{v#$f z92Ao`JpK!`r65*CrzhU+IbxYAbz~jo!Fq&(jrfD}GCRdhfhlD(2U;_Ys%Eb&B&x4i z{D;3BvreaLR@RFMTn?q2frE=Y0@`VizA6T7?v_*MmMbio!Mf4!{vwH!KraU}_*Y@rG}GpzLIUY5{i(H)p&ImsVNxuM|&F8OSqG{3;W`n{pC>Rws*`E8-o zz1~mth6FkiQI+6ZA+6Ze6&r9!@70yQ#k>8dgVFW%v?6f&IsAjL&Fx9iY!Gby)RZSE zvbs3gIZ%bF8V7|&pl~1T(kL75mz~>1E4Wy9UVFd35&Nv=WOlXsJYI_oVMZBi>M|<+ zw4r|UDsS}hMKqWG+&>;m%7EL|$MxCGp#O$2&2l!X0=?M0GF){Rg5wz7IhcvT3f|+E1{6LYLCC0>TyH%YNt3IGB8p%K@>>Neu$0% z##ejR1#9oQSJqd~B4WTpSzZjNHi%(9sMeSU?Jze(JUR^M^eUt3HC*KS<=4H}^XBTD zXh4vf%K(Y-cx}vunxLDfTWLUt7i-V z&3qgTfy`XRDZElcfXe!m{`&B-c>`Dta-$2tMDuub1sEB<<^K7<%2_a@U@${(|J{~O z+{wdu{#Q3Bve7no#wk(#O+*{qRqsw!!;Y1W9Zx#%S= zP0LCxds@}pjR)TgGQ{7Qn9j#nYw4S+&r?t{REz3@-R434$p2U`8wvr)mZ;`{U63NO z=oxd4i=bIjCP~rM1jDO+=`)9y#M%KjjTiYwO-iCBzVHro^*Fe zGPC$RQ0>SyxzWNuW=vg;rPm;k{q|N@Gq+?wD@&P`tahT%PbJ!^#Z~b^ZJtmFP7{C zYY#l}L~9hNsm|yWJ6BkLz3cgcrE&7nsVtYy9NDwwOSL|@YvnZ3wFKffNP4Z17IZLNoHw{(ns4YD~7 z-iWNInqCvBejVX)=KSu3&Nx3|dQ<(LcmvmM8pAsvOD|E3&a1qgEPOQU9=4wAh7<3Q zD9}`osI-?+5D%y@Z#&fET^L!09BnB;f3(o|t1Gy8AtOFuM$$9As z)5iqM*N+p#AF$^UzHb`;A^mKWHm&f3F6d|1WlaJ1#+5a9rXV-g(UC+&O+8;;WGqOM zni;cahenMA<5L)P!MJAByuA!WJ^YEyDVrOW0hP=3?#rg>UrbYa4g~!wwDjoM*l)(b z@l#YH zN_ApxZStx|W5mfuQB^j<^JLFOGGkz#VYInghQrEXvgNqVRjv^1;N42G?9@HGJk717H*GEl4|bJX*7<+MPM$avgHyPxLKX6l=rVd++7 zH?ohc#`#;pA0H(Nx5_%b8LZgLaE^$}4oEDUJSl8=15nIlOdLF&SBaPSd)KavRpl?G z%gU1j(h~sK3P}!b6|(E2maVr~{5+d=6)(nEq7Tias2qXhI;$~S1>@A{+-K8qjCz}@ z^OZ}dxbOZ_;~|$OkEPJ#mw!ETHG3Q5GI_-a^D7+F#4K`!*gn#%0Fa%FMFwZC@GLn$ ziPFr3W+sZI7Mj`w75ZL*9m)c|+nWO$qv21dL|un+K3i?8-P3{X$a5YR_ZK-ILZ5HY-Ly1y}NxwHOmiQrb3_ zUXYgO1;$0GCq?^Gvcytoy3p0vs-F0z?w=JJ(67#69&=AZ!3mgufY!oT4UCxBCz>9i z81-f$-v|qVGaU{}9_NB5fPMG;wHZ zVbybS6bw%c3~w-bmcp258sXpibDBMP*M%G3A$?%EAc0`<0^#nA-Iv>;1jR5Ji(F2iZm9v35;g!1+%*dlI_wOz%lTx9_q$;;ElSPEQ?C9h5D5|% zn74ADm)4FB&|qpS=xsw{3@1RVGq>3MG?wwbxymYdwTzo&iYlI9*!?m{x;FTwERgQn ztFxn*Qw1`hiWQ*VYiN4RLYXkxWouD_P8E!>{Hr^t9qJ*cgfwaA1)6Zv?> z!Z8z{zkm0b4Th1NW1QGRvd5GTX%@SCTAir@EvEyUm@ zp!|7@2Wb$N&h!XaUW{6WdnY5Py*P~HYPZ|sF)Au+c?`#JAo~x?Yd3FFw=qJ^a}SN8 z)nKS8LDgfZ)gKI)(Q4n+$eT*&5qcaDjH}FJ`fD-`OE=7V5d8pQ$ZDq>etrtu&w}7O zcPQEC8CKR1liLNLIHWY`0a#uh!j9Y_E&Q4t()0S}foFtTX`glLyL$BdtXLE&WhH!- zVbp_Do}mpbPXeT^&FTc>l`?(63Y?;BMp3ffGyUY9A=-+*gZ$I&1514^ibKW0DEHaN z-Uc?Pc@zVe?)@upRIR(={F%Ec_fr)i3Zu>nV*sm_?$<8OWF1frn7uwLfP6Vw=Vi~c z5I0xWFjrn&YzpM{$JtZPn&`Q=Is#dxodEq)p5pRRXs&;RnXfe#k(tIb;dn@ri!8V3l0>5 z)@QlGT`#`@rXyP`V2Y{oF1_BAyX7qaT(s`nL~_Adm6?EHn{vhdXala-h*ayld1k|% zi-&zUVnIMu?rgo#)k5B5S#(%$@W(Z37U`&<(5KOziw8AULCzSUJATr{Zf|kjP zK{#w!6oj`9t*LQPR8TOpn3d?!zwJm4W!7BatE#GIN4JKUmja3raqv;LLU69B*Oi1hra$O8kR#;P>^cpl9$xys1n<@=4d1bxr`Wvo;4!eFm zIYg|vHAVS2YSmq!F|e`ejSY;sUMkGHAE%}zqrKb`hw7?UgBO=2*YX%4==I{*uKT^$ z%li>muscK0uSL#t0nAW^b$o3*Uc(ei1mKgzGd@8;g9G@h7sz46-K$6Vf-Nhr&>^O3 z!p3sptLvt?Qw}r-HM%cHRDlp+bw{n@sQe9^$BT)7Jh$*V%YxkV(7Ij^@X%vwcwex#bNP+ozE0lA(G3yx(X43}$quRJ1SWi*FL~4U%55 zpiHf#Cex`afuE;WPfb=(o4ucUBPSMuqX2(BhqlPK%ZG2_H8Z$?2mI|*%u6ik%lmst z@ixK!--1q-->GDhFDxiX&_HLR#<0C&qC~0UZqH6Fo|}Ls0|9wvB%*+c8Y=3MzPla+ zww+o2rX~?tEWCM!$QUVPhji2lGAN9~#b(BWjSOn0jG^qCP_G2LBCyWmEO;dY4W4F?i^SV>iOrTcXlNdl=IOGxCg4wc}TYs~9$`#b8J+YCniL z*@0yn2E8|hF=rFpzPwtsnzNW*wXhbmvPG}kv&UlOc_iJHk6&c37Ig`~-FQyNxqnhv z#+dI^cufma5cwb2-wz~t5@@y{1Gq9>LLbqaVSu}8xo?7;?|5?;2E0jpr`1-}54A*h#!^+2)i8sO zT5TS_$r$nYrL}T(cdb=@Lk`#Tg@-@dl$!$EepbZU_}CY5kUwMg$?>9H13jFYQ*$ji zQsFDoqM*THH&bDPqS@)Eo1Lr(hXJ80AI~ux$pF{tbx=UA`RcIR3h}NQqd#T2nG=G| zx+7DyptfxLMcTDf>F1h-AlE6IA0Bl-@y&p+sY}#~?efg+Yt2tpT$t?t865y1T~nuh z@vA*(g7M_e7v~{A>#E=i^Shv8+o(hSEgo2B2C9=v)4^Qt{a1SV9UtdJ9Xz_JSkpM9)uAe-yu1RRCbCaZiCD;?1-D3 zU#j*g*Z<6-x|AapGvJ;DpwH01YEBT_Co2_dtAbps%kHZKAEXS|regNu+GtLsk>$A; z94HV3zNW5pOETAoROr92<38_ue+qaOpizO))e_LX+3=lRAz;fA_00A6EOiP?Xpek_ zwc+NGXYxKU`Pas#Fj8*in?BxKrdMsL%b*Ok#^6U;(l(@Vdcp>_%J{S^a8ghPlG_B{ zqrj{zpjOI$S(LV|>pM67sJqN@V64iOfDSvEL)>?tmfmUFl1`A~6H8iM1?!L%WsWHW zX)SM1Q`Z3p*6gV_?Fyb>HH~Kx76ij!+`Lrjof(MjQ}T$Jzrn$QlvUQmbXkM_dK2u8 zd^X9ImAh)(Ymq6rmRoCft*xd%Ws0mop5=SQR-K{a!Ca6h!A#y^13^H(-r*DXjB7<_ z-#8#?27jKP=rN|e&7uZSm zbW+zSRG%#*C*$9tvaSCqP#LOdTe~l|c9t*dCI~wcVnGK*olBr%6u=dx{VnaCoOcH8RQ^^j>?EniH$ya-6>+9HbG2cGs4+PGrQRb#_7 zGoS)`OiG@{}OU3Z=iVveOG}8LO*@`vg<88^ZdGn>9hxce$Rs@ zXDXKilQI;*4BpZr22p{{(7BBbYd>Pv8DO<_sV=78#|MI_Q#?=!X^ z$QUN2L6(v@CWslxtDBq<{elNwkPI=ez|CJ`C3%X0df5rXZIuX6ss3-GtelPW*Ilb0 z=vTG;@L@)dw9Z@tXECwnsk87o;M5Jt+-jy6$ zUO$;^zW+AKY$PBhh>0)lzB3K!QZ2jtCxAL+bz^-3)AVTv}0|gAOBcgOAA~w9H4lw0jB4`k-&Ri$4 zf51obY~vApgR4w;gq+AKXFsvx&X`VAUwqp6%Lv{W%rhI*EPd2>dF|BiH}lt+o=!QM z$v_X`P%E^%RdjU9^7Bn0D-wsYnA-!)>(ju7POY>{t$PYb#d&&YS-8r?hj)9Y{oMdN zx3qw?Ts1Si`FU=^x0Bodcc(!(W(G4)polo#!fd=drK>>GXZh6(RZzoV3MhtQl z0%F^@GC#oR0XiRz^2}qdO^ESg|3YOaMBP;KmuCZk98QXSq}7jD22`!C0BQqGP`WYR zHOLj~9Yi-(7ouc5362p+*8Vh$(>(@uMiZ(b?MJ3eo7va27QZ>&oi> z4V|7C^zYPk;rX9KSM#^BE#9B7QNz7m9Ytj{gp?JqPQpdfM8V^%=ag z$AT@#5p_Vp!?rUeUdJ4udDdU^>5^5;(`C&zpbVcqbbhYWMjj-c9tXNLMwFNMb^|CaO3eF)%if!y6>gUx${_F&UNLvA@H zB~MM-gNN-iqW|CE3~J4^Jq&dY|dZQB*N&|U45i!Hyg z;ROTEbIL^Vfc1VUMbv7bH>k2>@6bIX`lhkoEEv@8iwW+PyHyF=1-VPxG@Uz71#~Is zgKa?Z@a@+l^xqQjG$7tkTFtamJRk6!Ik>C+lChxn^BeDsXXq2 zf!|NFX^>CHYK zh+^ACMBBzPKV!zPi;tss*8>W^?&{K)oN>a{+k$EzXK~1K#7Y%_1u_H{ksRlNzZ?C6 zSLQk`*)HZ{6RD{`D=i-yu+BB}5%(K##25fUSwa4-*tAp!ktvtq4cveB4lM)mqfgaP z{py0w8s`h`_LkOnDFGunKR^GZ-(!S@z_D9+z|+|HBf}REnZ2@T>H?!CtV4+&zApa) zG-o99QF?IYT)90in-}{Lu?;T2!l}$5sB<&>i)F%zJJTSDG_ZSQKNbT3tsIlwC6jhz z;M~D%Iq|Ks4GCajO4s8!+VS%IlAs`|9}zfjPzz3V$OKc}cqK)|P0W{uth?mkKaPWp zo8rjyiCBP9FopR}57vcF#Gd~I+61lKEmmcA`!OZ~n*8UtQ~?XyROPOikDB$+b9BIc z%scWv3iNb(J>-m|gOLXLzgiO?`!DqS1u7ZNgY6f*pa}jew-0o0{k|;*Kkcb>dH6SU z*|RKgdi33_gwNj?{q%y}1eW>;q#=+{Qi7Now@j)e02?9B8b)OWg z&k@%c{8PbXjvgfYJjaTAO8?Xg;A`OZEWCzLdhR1Qt*U#3ufmjpH4ptphB-faTt@`N zoW5zwx!g>?Wc%_7E^p8^;`|I?UgH6*Y%a+XcHaS51RZ^;)AKR0er{!~J!^-?g;pjz z#xP)eJvmH9Z<+O~SRFM&DJ=v%rGe~ub=SHuj!OzU=>-O0OOFK}6{qcmmxzE3r4u5@ z5hFnXq&~aF?O?IjbqQW>97%&WfuHOe3_4C3TvPd&dB+E8U$6XFhYai!F}vf%cTyiJ zF13kpVWxb++`b^%5CJTea=aY0hb9KuQ4qQ>RZmZ;89O5KaOff0q5t(Gv zjF@_nWiA$^^?%Fan+*bCy#w|1a~;4@h^6ASz$SWz0@*niE78so&3b|d$jlFaOx4&C zzX-aNsW2;+Y_m4Zgrn}qnPD05N7NC3@#Z=r>4^{AC$+K0&cXuIfTR{g#XwsvbQDJ% zj7iSI4jJhLiDb)or!wt@BQ2k%du?M9bD7{f#P)TwMFdRD4rY&)>ech_>_jx>Rw}ee zO!@ZfH9idXu-T89Ek>XrkIdpSXs94|$kGhBVFw)aOZoKhmFW5T+0j;} zrBL)ikL8NpcNn-*u329`ssM2XakO60emWyy5u5{AW*;@)kUZoQTqWpT#%|jK+*i;! znPppopIQ12fLx$fDb4-^bK>6OK?Im#@p^+6gRbC5|T7uQyyDjW6)uqc*1)tP5|8^2k&3 z;ry9f4tpmLN0fQ>49-;Dk@601)Gp7Uelrk2=M&wY<`qvi$`2r{9I)!mHQrzWHe(Au^|V)x%I~lu=lCFul`?7g%w%jjb@i^(Pce4t1{!Jb zYyzABG%_ddvC$dVH=J->4-%~RWTaLEKMv8^3?z)&{ZFomMJt$RBnvf)o)S)8iXdM)j*Ea)Gtjw+_G>ZS-~?6nUJ{uV&jVa6Gv*LKZvkc2>N2L z+wu4+x>-e;CUj1Fj(Ene#I48~3F!mrI@O!v*dtAh%QHV_Cr!NZ&~^M$r=22(Kl92d zu56oNxi;_Hql!oEs!ZPW5Ah3*DLtifg>TNCA^7ER`$rmk?_#vuth5UT_Q@5)8>nyI ze`Jc@yEN-E0oR_B^K*d-{<7XpRZFG}XX%cPt4-$f{*$GyVAo?Gts9%A<%@n{kA`NZ zWvkslC`#@PFcnSFmV*>f60y9k7;F=H<~|b@+yn`klUf01AE{5Q%dp|w4cpk{-4YKR zH8%^C1>s;ik31Scfk&d0a8a%}Brc_quy6VlFb^m8$s_K_fyn2&RYfsDBHh1z2 zGJ!sde@Z4)cDS0~R9py`#(nrmIZkm-2*ON|ZWLLS4cFzJt2qHZO8+1kEM=kZE$JNf z{;}srq15ame{deSD{@RZ+J=f6_+^7CNPb5t6-GQ+Rs_F}(fF$6p?45OL=jQ{qf4#S z#cS5#ZB4kI6FAI%JGZQor`$LDFi0}knr7S9mB&c=7lqtf`GLSzX3sppV*N!}AA>6c z-k=$?F>)PIlVmILGfd_DMCIlUFTQu6!FFZ%QeoX2CJzKV#P+Y5)fKgsCH1h!2B@_| z4>ojeQlcLL2y1GvTI#?4-v+AGES_N-eI6c45i@KxolWbtnb%s4bQ!;Y*}pus=-7hT znG23M(8?M5Po3+-gE}_1lKWzR#7VW|#+#F~g3d11myq)61FLrJ!PUI(Nst6v7RG0;`#*|McU)0ymN$%z$1Hw#R`IJH~O61clfzMrE^&9Ak zLIY7WG#!Mx_FTO-VP9?o68M`yr-q?{)uFr1o3=os;lX6Vk*VHHEN;T1s5^z*BW(-U^aSkx9s!8m#u)&b6bS~ z_S~D$C@DvK)rN9ajOzPWOgj~5Exbp<`1@3X&bKmGOXvm5vm$pCfZVxHttVrWCG8NT zjD{2ts1{q-mhc-l?YwMJx+lg#M@L>^eL2t#<)mYizUn)ry2pXQ17w5bPIRxXq@)-P ztUofuLFE2NuDuG#v9_ndtDsF?j+Xd~I|c*`0LWUVrq`7JJRX`N^y4won;xjiI7H`& zUnr;)kl%#PyDgCq{Z}fZ^oZ2s1{XV0uiUWNk(eh^1RVjp2Jv=jpe@Ly`e72TXsONV z`O;Mm<$uHMawfeO0q!`Pv=iA^Kl4bAwqN|1cVpMpKCjAop8jJ!(hiWz02&;!GW=rA zt+&;xPl^z`Y;p}=y&5Cl3Ub)&f1?fbcDsU<(=-6bgmYrUlXrj|%k>ePukK+7JNy7P zC<)khvJiC!&}XX?RpQDf!QXy!6l|Y@bq0BYB&K`tMLJA>(EZ`CuJEBf4*{G_ChPLv zWChT|&Hko0vpw1jxJBKP82=|(gvsr_fD4v97<>=^VB%cJwULSLePNwGoTNd8hNqTf zTyu(AisnRVjmq?ovyuExz^&+(paHDPliQaRzxg-x3CPo)uC%~Ul}5fQQhbFyE4Yv! zX%|TTOFz;$l005Qni^B$wlB9o&SPh7RQ_t-&w+12i6}CHwrpxa#=16?AbN5T8pHPh zKQoT(18?wE!~{hwFv(PK<ag;jA!-IXdwIO}gspcTkzYG3&*!u|=1%{$Ia zr1Vw>l_}nwM`%<*h4r6f34aHF|Cb%Izscui;1V)q8oAg%%XB%`URpw%T`xSWA$AM1 zqhFrBCO)(CpSL3#XLhR5$<2wN)iiA9C*e=gBii%k@#i&ea|;H|UN~8V=mdvPtf#HW ziX(UM2a=p)vpOAbxu=pGu9=9RbdIIOSX#~=B??inq89m`yR9SP69b|*$f4r5p4S*S zcN|VF05Oun<8(mL419J4Qkvqc`QVNgri2~ymQ77+pvV#>W-KF5_XHpFLhL=wt29_B zjz<%l9_!wB?w^O-z>~ZxJH8c5l;zxda>d(Q*(L3ELJGbyA*)mBFXME*WGsn4Zn5x& z{$M~n^{=endm_?GutSypGcE+(rWLKum&x2@Tu|tKmun`n&47M5@BH4th(kDP!T@R} zX+Y*?i%7D?@W&$ma%EF2IZ}F{;|i#8wM8FbnU&hYn(SC5LlfbPCTHNeyQBrGfWr*b zm}O&V-2?I@T=8dc@Yf96ni$;4kW)tMX!O{%Yd#=g=2pY2euNK$JXdy4BE<084vw=v zVaJ92F7Pdn>XCC2M>LCQKyatj_?2pEoNGf=oeOy(x_e(MFE+74s)DA&qhcV=jM2XhQXcdYNW?moXvxe<5q0E8=$uJ|BtUX0f%z^?#?IB?n^}OWCFxLrm5&y!TTl zdVlZ#`pIUg{E-rnRl$L8SftQB17(iH$N}`sAZ&V+%~g@Maa>xsUlw1)(u{} z_N{OgHHA%4^&`xAEFattg{{%$G%xT}3XI;iQ7Lo^EJP#>j`>nTu9Qf3Vy@$7t!3Nq zxDb!@F#8XQf^)<1^_9@1)BU{Kba>Mp1}L~}6`;l(Mf>h0sAFJvK-7q6MB54)!Ts+X zY>xgOuOLhE#`Z&^L&kz<{XnnZN|Oh0Xxu5<6oqH^Lz++y`ai4uYme)nqk?Y96yu}` zYB)p55(J#mcNC;!*vw6bl_Kb0z;;-0)veECpBSI_k1rh*WCG4#%<(es?j~PbAHn8n zXZ6=j%kC(x8#_w(q*k|VjxMkD89=52=59~eu{%$-JmYafNJ+~1X4w2y^YWeDDOa>3 zu5em`w66a1sScJs$(O8qTxKVyDPcI-=iVhVT}ax{iW`;rrY0R<%6M+wgG#rGc&XS` zoqW{%+EKf$l?1Yqp4TEzg@`ru}iuErjeTH~E7b|58J_kym`?bN64pfT}J>t+sO+mG5ca!<)&!q-2P zt#x^zet7)=2aC>*v(Ova9xD2#NE&8--D+4-?4T)c4&M>iCv%Iy%`yIOBZEqClJ{P8 zYOy0L>z+DFyM(VFnpbXvZF&`c$LkCUrJ;?uYZU6J-ZdHO{WpIBKKYRmP&j%Ylo%}# zzYJXOESP6;e@CDf0enV;% z9&;;+kK*Rp(q9k`uG5vG%kYaDCA9T08y8)v@%`S{DAAl{L`_yy-p1NpdpqIGzQw}@DUGIuMav@o5ED+$k zwkA?4UcF<_Bh~t^+_b+cB2@Y|p&mDnPj>JFfN#zx!*DJR-n}fi7S73sHQq6{L@_Nkl0jrCV5WPv*7gs z60B(QNMHipAdU9=tBdO<0P3z+sOL+mf>G1+Si4F|-c9~sXRh5I1ze!S$z!_$LF8H& zWCDA=!a^}^i>B}VWYYD697U!e+l?8&kq*nj@t9cG0BzR=vnuG0d$J;mp`s@{%unSs zvb!e>55L#PIZI+%mfy64q_3N{f4%e+qGrIc535nr@}y@{yRV4WbY;4{C^EcjuHf+P zYiAZuP`1=@{-ojMh&9InvE(kNFE_6=7^c%qrV5=T3#?wdD8beI`;_2royCN((&VLU zI;F)gwLUDHV1hKY;jAz_Lo$3ok>XM)i0+TD9PWv&ZZ5X|FsJHKNTg+yr`;;wM^UXc zx>Qyeg?6lcJ!qSD1n8uQ8@B|akl-WUuRa0I0q(FfpvIUbZ9)LGhyq>USS64MZKjLR zTA*_eSk+=ys6idIYc%Z*;y^H4Y7>rl=sh7i3_vI{D@#$)1&X+&9|w{oP@HNV`=%`# zcF%-mOaxYH6%)_3%C-c$gw9DFITu4@<%er^ha^Cd)Q|iLHKoIUD`X0F679O8m}f`M zE0EBQ97u)`^#f>3S|#+wTG&Kjg^LTqN`cO90(3aixyUeSK{?g3>u37Mmd>Xazs%dGylzc#(WA*pt*g-Kody#ET1>8QkAgsWyA60Z>Wz+-)(Ak4TM1AFDq!79 zNi;RA9|0r3HS-nj>LlPbhzO$JW<1+0iC##@REg)9ygL!Q;r$!3j$H_LlrF_=>Z@Y`M!D~&u4(#I(%-&4CZrhlX>3$5ukmp$q5kV z2VTMd9QTlOSKdj@b+P*j?v|k~FsTL^J{96oL~}8pdBBQjVnChf0VlDHcq+tlDGTYF z^%kCeV;Lz>`PXNH41uTIu#g4$O<48-?Tu7v!FV83c0r{&V!Q@J_Gl!rp(MXnNUvjP z8gVPa8@}IZ{4}lC8T9t$I~OoCssi zW2MorfdjN&EEJ&K9ci8c?<6~8ON8eI^z?gkoX!wxh0omnG1$`=0Tz>TR)!XeFjPv) zsmFR+?d?q3`N^?wG8CzKohq{D>0~HZl%jUSc?z7SVW|3wtEAbtb*8QOVqOusxIMA8 zcQ7=M1*(*ZRaKfE&U}2vGp)Pv^we!5XRCdw@B1!*BhQVBMmFHr7NGhj*O2coiy4@! zY{{g(3rlTWZGJO-#4T-XAG zV0~fsjo1h=ordY1o~Yh;Qc#%O(|K6F@Sk5p7WxygtN{Y8wPjwQLih9whC2*=`!er4 zghpZ+V^m8>@z^CFkTtEV^;~Kq+Kx=z+6?Qn5SDvZ2w(xrL85hKUA$o+?ZV{3G~c~l~zR=UOrcJ4OhB)PeWmrRh~XzJ9X=NCl%j5448sv?)N(({XNsU2fFIfPC-hn zXD%VmRLPQQv%~a6JM#!jt1!3du=k!u`OM;ru{mbAqVD6LbG2ug$hKEk5yV1?J7oj& za$7_k4q-0bMUh*`|NOBuOlU&2N?Iv0QB9qXEBprxCWMo7CYoS^>CD-PFEkE3s zhu2C9|7tnkoSA@UZ>(@dwW9VP_Z|IuzNTK2(&z_<%n7_ZQThsR zT%Q;Ea8&YWtvS4Qb^#UYhTmJ~Q0)DveJ{U1+Ci1M9GJLWQC9<5P6Qz#v{ry!by?}; zNjxKfHT{Pt)ps1hw&AS|DkMNq6(W6TjT^J!dT$ zs$B}=7L^CdEhP9n_icv4zo&D<@{8@g`CZ^Nxm(>+h5cX5g$YLFmaV`GCD*3?Sp;(I z^h8b1Ov;uAN&saBxR;aWM5oq9L17o!?Pq|t8R$XE<$;4p8bf|!+2;1Gd+&lxtRUa)T1Wd|X#}@TMNzQZD4r$&=&eXP7 zst^0>U51)sj_zMoB^9}-DzdH}$nZqCcFt8X;N#CUA8x5V-!-sCRKLIl+DEtl==g>N zbl@X5Q=e-UqImWk4K4Vb*#FT&L>5pV1yu=867>%r0ZCG^d>7NHPv)lNCA>g~qAfZL z{e%H&>+EPvHb1a=&PZ$V>t5hT+TOl>HtyS{^{ygfu>ZRS=Vku=rKq0g^gyGtCmp$B+iZ`-0F^1aE_d64YNsI8jG= z;Sc-6_#a=5gH7m$92X{g6rNc{j4H7#8?33TIdCP`kLDAFucc$)Zq$i+bWHhil0+FS zQSX#j(%J6UDS`)v+-N358DiQw~QkK4dkrNrn#M9e>88D(zKg#NRZ+=`Sfyu1f*a{LM6B`3b8)XyHy~~`jzLFW>~bC6 z6}0%?NpRe{Rp?11cZ_TV2x5dyBjQr4wJ~m4Hw`+L#|A^gnOnuSvHZm$QoV?1VHnT>ECek z$qc{4a_%NOsm=&aB(7I9@Up+l_HAN2A#!jB`05$5n!u`AE27_VEo@I-|32=m-3@-8 zM&bOsQ1WIed5!S0#~Al$>1!|hzgR?I&>_cJ8S{aaA^9lUs1~!~am+t`>fBa5ert3I zhgv`dVCr)s6(0PpPqcKZ3}#%i5lqB}+bz2k@~+>tx>d@)D4j>2LBrE99W^x#)xX3W zp;sjz2jrJ%HP4qMUusumJa=^$Q4Ctn3hD>lG<(m2$1&l#;%f3fT3Hh_#4~1Z7*LbB z3W%v%p#nGHN5=*It(kH`l7C-gIK^bx;+5N|5zaUgrE#WtHuML3fUpa{tmfLC~iluo^n)_~CgyEol<%a;z+!UUAkD)`_=xwkS0K6uUXd9lDNgyVOJCdz7*j zQ7Dg>V3pIW(#wAU9HN`-yu+JYf8q^rCvf&OB+p5vUkn0^~^Y%o$25x%dR-P zH1G#!xkz1X_rKzMaEDXWg91Od^GaS1%|aiMEo8j@`Z<$p^V($lDu5~97g4;^u5X`7 z8YW(}H-sW$$EM?fC0EziTkWYTM~z-P!P2b8Kr`4doOsrfItoUsD`BJDZxoF3f9_$| zFV59m(NHFBQ+2w$RWIdenU*yy{VQy_Nmy&rC!KDWWM>@V&*@fq|1lxIyq&%KaI$yDFwNM z&eLUv#8*y(E+mhhOIPCC-`v=a?N(cLW8E~H4;pqjM0!OR!s}S}j4Fg6kGP-->J@Fd z$Q`#Vv!pk@ZaKs4+7{zt#eLJKnwXC=+T37jv%Qz$5M8(nFp+N6{F0_avvE=wy{QOh zDj>4gjMr6f?^j^u##5xMzP8kkhj4lhrl?6g;0$C!d2v7K3B$T6l08cW*W@3>jFx?I z*J1{oB%wPnZ$JxAaz_#}4}wF%>e#zsikIDtj|8N>5)bIGl;u+8#OsBliH=<6g^}fO z%&zk|7=Lrk0p&08ZwwC}DNefKH#sPBbqO$WFV7nTU`0b&>~y!n2oXovCASY|ofAC{ zi7oRqdK_eBy39rOVyzNJt=p2kw+VC?Xtv>rm@+#26x%=aoqduLYUGxze_MOA;*Wn^ zB%ArZ2$Tw7gkp=vUVEkrid>3k6{PNZoOfZw5mB%Ul+eD2sawWip$Af7PGmV?fNKJ$ zAh(j?^azO3@!yNp2_{)G=!biPKFlS_<+U#l&3NB&JYa%-P7z}Bb((UXf_6?DV0+lK zgfFCySzp}2(`yayQ3pB36M%0KV}<8x1|DS7%C+J<52~K#elPuZ=R`8ZTPasxV}gI2 z_I$PkM;Pk*Onsgl7&G%#>A=FoXk7(eV5c(ZYs?Y2%YP9-^7Kb+GCx$Z;3t2a{S4qwZWo_R|=**!_XT}M`;9BuP8pGrEw zM@z&xC_Ym@JVA!T(;4b{1!tW4B8b@jTTk75`U)bWk-Xyq8uJ(0iU%G%ifWA+jdRgJ zn-oAMF*ILf0<)a%eU#i|qoTj!*}~qDX9h@I%9j-dWz%`^>+2i`>%CmZvtlueu6^%% z6><0Kly$7LebRKSzd%fz?*?kpyn=csji0(!c87fH-3iD@$tj&^Ok-eGTVW;F=)S+2 zL36M6yYDzH6e?XngS<|0T|!`nQog*UsI4Nz9%iBYI@=6)r(5lO!c&9&2n~&riY?8s zf4^qvSGmM^A|gvNos@RZ_A`<%LIo*bgRlXOSeZwrfvC?oyGSo`{G+RA!ti^bh)=R(vh7CffR2nSF>GE zVmLrJ*gz8c`&G0iD)8^VD2x5*!(oO`*h5Z9MV&d-zBBfp+m$@<%kkUV#Xls;0R%WV zdbjai#R#S^#aptw#bZ(!6vTMz_ws8}vtP5)=RH%QNFj>-!Tui~zu?_#;C+$I2&)m~ zQePFe`pXh>q98=lvf8@x!w48Wd>ga`&^Z@D)`{!fgd8_a$U`z9BFT*hGakkbxse(%0Da#&W13cqkEHGR%f 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

rR!waTzmD?t;hL=0Y9l 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⁡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

$Kx0PAUtC!u+q>q@6)U|z3I{?|xq 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�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%0DSxr`^o=v)!ws= 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

?b{U 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 literal 1233949 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∈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≷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

IVrnh+_ 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

=ZP)yfuUmxCQxZiTQAb1t7c*t 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 wqXzs(sR5HFXh!bO$o)wu@zv@7wcMW Date: Thu, 23 Nov 2023 20:05:33 +0530 Subject: [PATCH 065/443] fix: return none instead of err when payment method data is not found for bank debit during listing (#2967) --- crates/router/src/core/payment_methods/cards.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 85a0ca5f2441..044e270a7ea9 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2459,8 +2459,7 @@ async fn get_bank_account_connector_details( })) } }, - None => Err(errors::ApiErrorResponse::InternalServerError.into()) - .attach_printable("Unable to fetch payment method data"), + None => Ok(None), } } From 3322103f5c9b7c2a5b663980246c6ca36b8dc63e Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:33:30 +0530 Subject: [PATCH 066/443] feat(router): add `connector_transaction_id` in error_response from connector flows (#2972) --- crates/data_models/src/payments/payment_attempt.rs | 1 + crates/diesel_models/src/payment_attempt.rs | 3 +++ crates/router/src/connector/aci.rs | 1 + crates/router/src/connector/adyen.rs | 7 +++++++ crates/router/src/connector/adyen/transformers.rs | 5 +++++ crates/router/src/connector/airwallex.rs | 1 + crates/router/src/connector/authorizedotnet.rs | 3 +++ .../src/connector/authorizedotnet/transformers.rs | 4 ++++ crates/router/src/connector/bambora.rs | 1 + crates/router/src/connector/bankofamerica.rs | 1 + .../src/connector/bankofamerica/transformers.rs | 3 +++ crates/router/src/connector/bitpay.rs | 1 + crates/router/src/connector/bluesnap.rs | 3 +++ crates/router/src/connector/boku.rs | 2 ++ crates/router/src/connector/braintree.rs | 2 ++ .../braintree/braintree_graphql_transformers.rs | 1 + crates/router/src/connector/cashtocode.rs | 1 + .../router/src/connector/cashtocode/transformers.rs | 1 + crates/router/src/connector/checkout.rs | 1 + .../router/src/connector/checkout/transformers.rs | 2 ++ crates/router/src/connector/coinbase.rs | 1 + crates/router/src/connector/cryptopay.rs | 1 + crates/router/src/connector/cybersource.rs | 1 + .../src/connector/cybersource/transformers.rs | 1 + crates/router/src/connector/dlocal.rs | 1 + crates/router/src/connector/dummyconnector.rs | 1 + crates/router/src/connector/fiserv.rs | 2 ++ crates/router/src/connector/forte.rs | 1 + crates/router/src/connector/globalpay.rs | 2 ++ crates/router/src/connector/globepay.rs | 1 + .../router/src/connector/globepay/transformers.rs | 1 + crates/router/src/connector/gocardless.rs | 1 + crates/router/src/connector/helcim.rs | 1 + crates/router/src/connector/iatapay.rs | 2 ++ crates/router/src/connector/klarna.rs | 1 + crates/router/src/connector/mollie.rs | 1 + crates/router/src/connector/multisafepay.rs | 1 + .../src/connector/multisafepay/transformers.rs | 3 +++ crates/router/src/connector/nexinets.rs | 1 + crates/router/src/connector/nmi/transformers.rs | 1 + crates/router/src/connector/noon.rs | 1 + crates/router/src/connector/noon/transformers.rs | 1 + crates/router/src/connector/nuvei/transformers.rs | 1 + crates/router/src/connector/opayo.rs | 1 + crates/router/src/connector/opennode.rs | 1 + crates/router/src/connector/payeezy.rs | 1 + crates/router/src/connector/payme.rs | 1 + crates/router/src/connector/payme/transformers.rs | 2 ++ crates/router/src/connector/paypal.rs | 3 +++ crates/router/src/connector/payu.rs | 2 ++ crates/router/src/connector/powertranz.rs | 1 + .../router/src/connector/powertranz/transformers.rs | 2 ++ crates/router/src/connector/prophetpay.rs | 1 + .../router/src/connector/prophetpay/transformers.rs | 5 +++++ crates/router/src/connector/rapyd.rs | 1 + crates/router/src/connector/rapyd/transformers.rs | 2 ++ crates/router/src/connector/shift4.rs | 1 + crates/router/src/connector/square.rs | 1 + crates/router/src/connector/stax.rs | 1 + crates/router/src/connector/stripe.rs | 13 +++++++++++++ crates/router/src/connector/stripe/transformers.rs | 7 +++++++ crates/router/src/connector/trustpay.rs | 3 +++ .../router/src/connector/trustpay/transformers.rs | 8 ++++++++ crates/router/src/connector/tsys/transformers.rs | 1 + crates/router/src/connector/volt.rs | 1 + crates/router/src/connector/wise.rs | 4 ++++ crates/router/src/connector/worldpay.rs | 1 + crates/router/src/connector/zen.rs | 1 + crates/router/src/core/payments/access_token.rs | 1 + .../core/payments/operations/payment_response.rs | 1 + crates/router/src/core/payments/retry.rs | 1 + crates/router/src/services/api.rs | 3 +++ crates/router/src/types.rs | 3 +++ crates/router/src/types/api.rs | 1 + crates/router/src/utils.rs | 1 + crates/router/src/workflows/payment_sync.rs | 1 + crates/storage_impl/src/payments/payment_attempt.rs | 4 ++++ 77 files changed, 153 insertions(+) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 80ae283be85b..b866237745fb 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -326,6 +326,7 @@ pub enum PaymentAttemptUpdate { updated_by: String, unified_code: Option>, unified_message: Option>, + connector_transaction_id: Option, }, CaptureUpdate { amount_to_capture: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 82ab9a1c02e1..42af827f522b 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -243,6 +243,7 @@ pub enum PaymentAttemptUpdate { updated_by: String, unified_code: Option>, unified_message: Option>, + connector_transaction_id: Option, }, CaptureUpdate { amount_to_capture: Option, @@ -543,6 +544,7 @@ impl From for PaymentAttemptUpdateInternal { updated_by, unified_code, unified_message, + connector_transaction_id, } => Self { connector, status: Some(status), @@ -556,6 +558,7 @@ impl From for PaymentAttemptUpdateInternal { tax_amount, unified_code, unified_message, + connector_transaction_id, ..Default::default() }, PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f51c91f441df..f6384bf0a5c5 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -79,6 +79,7 @@ impl ConnectorCommon for Aci { .join("; ") }), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 676f15d2f564..e101b796b8d4 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -74,6 +74,7 @@ impl ConnectorCommon for Adyen { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -256,6 +257,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -375,6 +377,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -546,6 +549,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } @@ -716,6 +720,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -920,6 +925,7 @@ impl message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -1439,6 +1445,7 @@ impl services::ConnectorIntegration { @@ -929,6 +931,7 @@ fn get_error_response( reason: Some(message.to_string()), status_code, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 884504154e8f..2c8a63a53e5c 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -574,6 +574,7 @@ impl reason: None, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }) }); let metadata = transaction_response @@ -649,6 +650,7 @@ impl reason: None, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }) }); let metadata = transaction_response @@ -792,6 +794,7 @@ impl TryFrom types::Error reason: None, status_code, attempt_status: None, + connector_transaction_id: None, } } diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index ff6fdcb46769..19849763ed8e 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -96,6 +96,7 @@ impl ConnectorCommon for Bambora { message: response.message, reason: Some(serde_json::to_string(&response.details).unwrap_or_default()), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index b6e19fa0d296..a01ea72338c5 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -233,6 +233,7 @@ impl ConnectorCommon for Bankofamerica { message, reason: Some(connector_reason), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 8af7cfd6c45e..70db9a6d8797 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -540,6 +540,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -596,6 +597,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -652,6 +654,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index 856d0a9ec9d7..b6bbaafc4a38 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -121,6 +121,7 @@ impl ConnectorCommon for Bitpay { message: response.error, reason: response.message, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index d1aa1fa25ee6..0bc56d4e9955 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -127,6 +127,7 @@ impl ConnectorCommon for Bluesnap { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, } } bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { @@ -135,6 +136,7 @@ impl ConnectorCommon for Bluesnap { message: error_res.error_name.clone().unwrap_or(error_res.error_code), reason: Some(error_res.error_description), attempt_status: None, + connector_transaction_id: None, }, bluesnap::BluesnapErrors::General(error_response) => { let (error_res, attempt_status) = if res.status_code == 403 @@ -156,6 +158,7 @@ impl ConnectorCommon for Bluesnap { message: error_response, reason: Some(error_res), attempt_status, + connector_transaction_id: None, } } }; diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 87e8fd0eb96a..a2ae9d628134 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Boku { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }), Err(_) => get_xml_deserialized(res), } @@ -668,6 +669,7 @@ fn get_xml_deserialized(res: Response) -> CustomResult Ok(ErrorResponse { @@ -141,6 +142,7 @@ impl ConnectorCommon for Braintree { message: consts::NO_ERROR_MESSAGE.to_string(), reason: Some(response.errors), attempt_status: None, + connector_transaction_id: None, }), Err(error_msg) => { logger::error!(deserialization_error =? error_msg); diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index bf51973237c5..5069a9fe38d2 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -317,6 +317,7 @@ fn get_error_response( reason: error_reason, status_code: http_code, attempt_status: None, + connector_transaction_id: None, }) } diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index a8d7d6d80504..6749f4189340 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -120,6 +120,7 @@ impl ConnectorCommon for Cashtocode { message: response.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 42e47c077e8c..cfca998e06c3 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -218,6 +218,7 @@ impl message: error_data.error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index ca2556544f90..312a91196de7 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -132,6 +132,7 @@ impl ConnectorCommon for Checkout { .map(|errors| errors.join(" & ")) .or(response.error_type), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 6ad040da2842..90e65c8b0474 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -577,6 +577,7 @@ impl TryFrom> .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, attempt_status: None, + connector_transaction_id: None, }) } else { None @@ -625,6 +626,7 @@ impl TryFrom> .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), reason: item.response.response_summary, attempt_status: None, + connector_transaction_id: None, }) } else { None diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 9c0a06a52c90..b294a4474f69 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -109,6 +109,7 @@ impl ConnectorCommon for Coinbase { message: response.error.message, reason: response.error.code, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index 417a36145b92..2af40a298ce0 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -168,6 +168,7 @@ impl ConnectorCommon for Cryptopay { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index ce283b12b798..1868611184f9 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -137,6 +137,7 @@ impl ConnectorCommon for Cybersource { message, reason: Some(connector_reason), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 0e81b6b59dff..33b8fa56d00e 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -552,6 +552,7 @@ impl reason: Some(error.reason), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 4ae3a292fdae..28ae058286f0 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -136,6 +136,7 @@ impl ConnectorCommon for Dlocal { message: response.message, reason: response.param, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index 9edcd957ff09..961ef005f2f3 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -112,6 +112,7 @@ impl ConnectorCommon for DummyConnector { message: response.error.message, reason: response.error.reason, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 2bdb7177d941..28b6d932760d 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -152,6 +152,7 @@ impl ConnectorCommon for Fiserv { reason: first_error.field.to_owned(), status_code: res.status_code, attempt_status: None, + connector_transaction_id: None, }) }) .unwrap_or(types::ErrorResponse { @@ -160,6 +161,7 @@ impl ConnectorCommon for Fiserv { reason: None, status_code: res.status_code, attempt_status: None, + connector_transaction_id: None, })) } } diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 3aa7cee32878..948db00c936f 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -131,6 +131,7 @@ impl ConnectorCommon for Forte { message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index 26494d349b88..39452e53df17 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -105,6 +105,7 @@ impl ConnectorCommon for Globalpay { message: response.detailed_error_description, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } @@ -319,6 +320,7 @@ impl ConnectorIntegration reason: Some(error_response.error_info), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }), @@ -810,6 +811,7 @@ impl TryFrom for types::ErrorResponse { reason: None, status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index b6ed231e5b50..457928642554 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -137,6 +137,7 @@ impl ConnectorCommon for Noon { message: response.class_description, reason: Some(response.message), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 27a874930bcc..5ff92582051a 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -512,6 +512,7 @@ impl reason: Some(error_message), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), _ => { let connector_response_reference_id = diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index c23114e2a96b..25562f54bfeb 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1580,6 +1580,7 @@ fn get_error_response( reason: None, status_code: http_code, attempt_status: None, + connector_transaction_id: None, }) } diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index ba0fb2046b7c..73a793adcf70 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -108,6 +108,7 @@ impl ConnectorCommon for Opayo { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index 41d1e6c3d88c..c4f3d3682dca 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -111,6 +111,7 @@ impl ConnectorCommon for Opennode { message: response.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 33a8ec65152e..0be640f8fbe4 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -124,6 +124,7 @@ impl ConnectorCommon for Payeezy { message: error_messages.join(", "), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index 1e67f8a9f350..84367b3a96f6 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -98,6 +98,7 @@ impl ConnectorCommon for Payme { response.status_error_details, response.status_additional_info )), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 24b7f2b3a0bd..092a8b49fd86 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -227,6 +227,7 @@ impl From<(&PaymePaySaleResponse, u16)> for types::ErrorResponse { reason: pay_sale_response.status_error_details.to_owned(), status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } @@ -310,6 +311,7 @@ impl From<(&SaleQuery, u16)> for types::ErrorResponse { reason: sale_query_response.sale_error_text.clone(), status_code: http_code, attempt_status: None, + connector_transaction_id: None, } } } diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 0e8cff8c0569..4e50bc924b33 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -92,6 +92,7 @@ impl Paypal { message: response.message.clone(), reason: error_reason.or(Some(response.message)), attempt_status: None, + connector_transaction_id: None, }) } } @@ -245,6 +246,7 @@ impl ConnectorCommon for Paypal { message: response.message.clone(), reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -380,6 +382,7 @@ impl ConnectorIntegration reason: Some(item.response.response_text), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }) @@ -467,6 +468,7 @@ impl reason: Some(item.response.response_text), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }) @@ -515,6 +517,7 @@ impl reason: Some(item.response.response_text), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }), ..item.data }) @@ -625,6 +628,7 @@ impl TryFrom TryFrom { logger::error!(deserialization_error =? error_msg); diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 08985ba022fc..898b6ed6d147 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -458,6 +458,7 @@ impl message: item.response.status.status.unwrap_or_default(), reason: data.failure_message.to_owned(), attempt_status: None, + connector_transaction_id: None, }), ), _ => { @@ -499,6 +500,7 @@ impl message: item.response.status.status.unwrap_or_default(), reason: item.response.status.message, attempt_status: None, + connector_transaction_id: None, }), ), }; diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index 6f3a2b802014..dfb4a7de0811 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -100,6 +100,7 @@ impl ConnectorCommon for Shift4 { message: response.error.message, reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index d836285755d4..1f1dee6b9e1b 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -124,6 +124,7 @@ impl ConnectorCommon for Square { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: Some(reason), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 024211c8caaa..1a0cc54a128d 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -110,6 +110,7 @@ impl ConnectorCommon for Stax { .to_owned(), ), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index ccf843ec78d6..475105c9cebe 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -227,6 +227,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -357,6 +358,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -483,6 +485,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -617,6 +620,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -760,6 +764,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -918,6 +923,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1041,6 +1047,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1197,6 +1204,7 @@ impl .unwrap_or(message) }), attempt_status: None, + connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), }) } } @@ -1318,6 +1326,7 @@ impl services::ConnectorIntegration .or(Some(error.message.clone())), status_code: item.http_code, attempt_status: None, + connector_transaction_id: None, }); let connector_metadata = @@ -2788,6 +2789,12 @@ pub struct ErrorDetails { pub message: Option, pub param: Option, pub decline_code: Option, + pub payment_intent: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct PaymentIntentErrorResponse { + pub id: String, } #[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)] diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 65ab5a7ba58d..2430aac6c19f 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -139,6 +139,7 @@ impl ConnectorCommon for Trustpay { .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), reason: reason.or(response_data.description), attempt_status: None, + connector_transaction_id: None, }) } Err(error_msg) => { @@ -298,6 +299,7 @@ impl ConnectorIntegration TryFrom( updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), + connector_transaction_id: err.connector_transaction_id, }), ) } diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index f16f7629578b..c5501ab4dc3b 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -415,6 +415,7 @@ where updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), + connector_transaction_id: error_response.connector_transaction_id.clone(), }, storage_scheme, ) diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index aae17195517d..5481d5c5cf9d 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -224,6 +224,7 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, pub status_code: u16, pub attempt_status: Option, + pub connector_transaction_id: Option, } impl ErrorResponse { @@ -992,6 +993,7 @@ impl ErrorResponse { reason: None, status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), attempt_status: None, + connector_transaction_id: None, } } } @@ -1035,6 +1037,7 @@ impl From for ErrorResponse { _ => 500, }, attempt_status: None, + connector_transaction_id: None, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index b7d2fc8db33e..bcb3a9add553 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -114,6 +114,7 @@ pub trait ConnectorCommon { message: consts::NO_ERROR_MESSAGE.to_string(), reason: None, attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 83586e51d66a..901e84997e67 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -405,6 +405,7 @@ pub fn handle_json_response_deserialization_failure( message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), reason: Some(response_data), attempt_status: None, + connector_transaction_id: None, }) } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index c4b35cd6301a..04f91f30bc7e 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -140,6 +140,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { updated_by: merchant_account.storage_scheme.to_string(), unified_code: None, unified_message: None, + connector_transaction_id: None, }; payment_data.payment_attempt = db diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 238a2d75087c..0526fcec9c53 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1325,6 +1325,7 @@ impl DataModelExt for PaymentAttemptUpdate { updated_by, unified_code, unified_message, + connector_transaction_id, } => DieselPaymentAttemptUpdate::ErrorUpdate { connector, status, @@ -1337,6 +1338,7 @@ impl DataModelExt for PaymentAttemptUpdate { updated_by, unified_code, unified_message, + connector_transaction_id, }, Self::CaptureUpdate { multiple_capture_count, @@ -1588,6 +1590,7 @@ impl DataModelExt for PaymentAttemptUpdate { updated_by, unified_code, unified_message, + connector_transaction_id, } => Self::ErrorUpdate { connector, status, @@ -1600,6 +1603,7 @@ impl DataModelExt for PaymentAttemptUpdate { tax_amount, unified_code, unified_message, + connector_transaction_id, }, DieselPaymentAttemptUpdate::CaptureUpdate { amount_to_capture, From e0bde433282a34eb9eb28a2d9c43c2b17b5e65e5 Mon Sep 17 00:00:00 2001 From: Vedant Khairnar Date: Fri, 24 Nov 2023 14:29:29 +0530 Subject: [PATCH 067/443] docs(README): Updated Community Platform Mentions (#2960) Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8c5ad9e03b2d..e820b93e63cc 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,8 @@ We welcome contributions from the community. Please read through our Included are directions for opening issues, coding standards, and notes on development. -🦀 **Important note for Rust developers**: We aim for contributions from the community +- We appreciate all types of contributions: code, documentation, demo creation, or something new way you want to contribute to us. We will reward every contribution with a Hyperswitch branded t-shirt. +- 🦀 **Important note for Rust developers**: We aim for contributions from the community across a broad range of tracks. Hence, we have prioritised simplicity and code readability over purely idiomatic code. For example, some of the code in core functions (e.g., `payments_core`) is written to be more readable than @@ -264,10 +265,9 @@ pure-idiomatic. Get updates on Hyperswitch development and chat with the community: -- Read and subscribe to [the official Hyperswitch blog][blog]. -- Join our [Discord server][discord]. -- Join our [Slack workspace][slack]. -- Ask and explore our [GitHub Discussions][github-discussions]. +- [Discord server][discord] for questions related to contributing to hyperswitch, questions about the architecture, components, etc. +- [Slack workspace][slack] for questions related to integrating hyperswitch, integrating a connector in hyperswitch, etc. +- [GitHub Discussions][github-discussions] to drop feature requests or suggest anything payments-related you need for your stack. [blog]: https://hyperswitch.io/blog [discord]: https://discord.gg/wJZ7DVW8mm From 97a38a78e514e4fa3b5db46b6de985be6312dcc3 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:18:10 +0530 Subject: [PATCH 068/443] fix(core): Error propagation for not supporting partial refund (#2976) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connector/prophetpay/transformers.rs | 5 +- crates/router/src/core/errors/utils.rs | 90 +++++++++++++++---- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index a2c3c55c02b8..d81b931edfc9 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -583,10 +583,7 @@ impl TryFrom<&ProphetpayRouterData<&types::RefundsRouterData>> for Prophet action_type: ProphetpayActionType::get_action_type(&ProphetpayActionType::Refund), }) } else { - Err(errors::ConnectorError::NotImplemented( - "Partial Refund is Not Supported".to_string(), - ) - .into()) + Err(errors::ConnectorError::NotImplemented("Partial Refund".to_string()).into()) } } } diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index 869a5b6bde95..b62abd0e336e 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -136,25 +136,81 @@ pub trait ConnectorErrorExt { impl ConnectorErrorExt for error_stack::Result { fn to_refund_failed_response(self) -> error_stack::Result { - self.map_err(|err| { - let data = match err.current_context() { - errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => { - let response_str = std::str::from_utf8(bytes); - match response_str { - Ok(s) => serde_json::from_str(s) - .map_err( - |error| logger::error!(%error,"Failed to convert response to JSON"), - ) - .ok(), - Err(error) => { - logger::error!(%error,"Failed to convert response to UTF8 string"); - None - } + self.map_err(|err| match err.current_context() { + errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => { + let response_str = std::str::from_utf8(bytes); + let data = match response_str { + Ok(s) => serde_json::from_str(s) + .map_err( + |error| logger::error!(%error,"Failed to convert response to JSON"), + ) + .ok(), + Err(error) => { + logger::error!(%error,"Failed to convert response to UTF8 string"); + None } + }; + err.change_context(errors::ApiErrorResponse::RefundFailed { data }) + } + errors::ConnectorError::NotImplemented(reason) => { + errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + reason.to_string(), + ), } - _ => None, - }; - err.change_context(errors::ApiErrorResponse::RefundFailed { data }) + .into() + } + errors::ConnectorError::FailedToObtainIntegrationUrl + | errors::ConnectorError::RequestEncodingFailed + | errors::ConnectorError::RequestEncodingFailedWithReason(_) + | errors::ConnectorError::ParsingFailed + | errors::ConnectorError::ResponseDeserializationFailed + | errors::ConnectorError::UnexpectedResponseError(_) + | errors::ConnectorError::RoutingRulesParsingError + | errors::ConnectorError::FailedToObtainPreferredConnector + | errors::ConnectorError::ProcessingStepFailed(_) + | errors::ConnectorError::InvalidConnectorName + | errors::ConnectorError::InvalidWallet + | errors::ConnectorError::ResponseHandlingFailed + | errors::ConnectorError::MissingRequiredField { .. } + | errors::ConnectorError::MissingRequiredFields { .. } + | errors::ConnectorError::FailedToObtainAuthType + | errors::ConnectorError::FailedToObtainCertificate + | errors::ConnectorError::NoConnectorMetaData + | errors::ConnectorError::FailedToObtainCertificateKey + | errors::ConnectorError::NotSupported { .. } + | errors::ConnectorError::FlowNotSupported { .. } + | errors::ConnectorError::CaptureMethodNotSupported + | errors::ConnectorError::MissingConnectorMandateID + | errors::ConnectorError::MissingConnectorTransactionID + | errors::ConnectorError::MissingConnectorRefundID + | errors::ConnectorError::MissingApplePayTokenData + | errors::ConnectorError::WebhooksNotImplemented + | errors::ConnectorError::WebhookBodyDecodingFailed + | errors::ConnectorError::WebhookSignatureNotFound + | errors::ConnectorError::WebhookSourceVerificationFailed + | errors::ConnectorError::WebhookVerificationSecretNotFound + | errors::ConnectorError::WebhookVerificationSecretInvalid + | errors::ConnectorError::WebhookReferenceIdNotFound + | errors::ConnectorError::WebhookEventTypeNotFound + | errors::ConnectorError::WebhookResourceObjectNotFound + | errors::ConnectorError::WebhookResponseEncodingFailed + | errors::ConnectorError::InvalidDateFormat + | errors::ConnectorError::DateFormattingFailed + | errors::ConnectorError::InvalidDataFormat { .. } + | errors::ConnectorError::MismatchedPaymentData + | errors::ConnectorError::InvalidWalletToken + | errors::ConnectorError::MissingConnectorRelatedTransactionID { .. } + | errors::ConnectorError::FileValidationFailed { .. } + | errors::ConnectorError::MissingConnectorRedirectionPayload { .. } + | errors::ConnectorError::FailedAtConnector { .. } + | errors::ConnectorError::MissingPaymentMethodType + | errors::ConnectorError::InSufficientBalanceInPaymentMethod + | errors::ConnectorError::RequestTimeoutReceived + | errors::ConnectorError::CurrencyNotSupported { .. } + | errors::ConnectorError::InvalidConnectorConfig { .. } => { + err.change_context(errors::ApiErrorResponse::RefundFailed { data: None }) + } }) } From d56d80557050336d5ed37282f1aa34b6c17389d1 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:20:21 +0530 Subject: [PATCH 069/443] fix(router): mark refund status as failure for not_implemented error from connector flows (#2978) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- crates/router/src/core/refunds.rs | 39 ++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index b2f73c0b7ce7..aba6e9794e04 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -189,15 +189,48 @@ pub async fn trigger_refund_to_gateway( types::RefundsData, types::RefundsResponseData, > = connector.connector.get_connector_integration(); - services::execute_connector_processing_step( + let router_data_res = services::execute_connector_processing_step( state, connector_integration, &router_data, payments::CallConnectorAction::Trigger, None, ) - .await - .to_refund_failed_response()? + .await; + let option_refund_error_update = + router_data_res + .as_ref() + .err() + .and_then(|error| match error.current_context() { + errors::ConnectorError::NotImplemented(message) => { + Some(storage::RefundUpdate::ErrorUpdate { + refund_status: Some(enums::RefundStatus::Failure), + refund_error_message: Some(message.to_string()), + refund_error_code: Some("NOT_IMPLEMENTED".to_string()), + updated_by: storage_scheme.to_string(), + }) + } + _ => None, + }); + // Update the refund status as failure if connector_error is NotImplemented + if let Some(refund_error_update) = option_refund_error_update { + state + .store + .update_refund( + refund.to_owned(), + refund_error_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed while updating refund: refund_id: {}", + refund.refund_id + ) + })?; + } + router_data_res.to_refund_failed_response()? } else { router_data }; From 3db721388a7f0e291d7eb186661fc69a57068ea6 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:44:28 +0530 Subject: [PATCH 070/443] fix: surcharge related status and rules fix (#2974) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 3 +++ crates/api_models/src/payments.rs | 3 +++ .../src/payments/payment_attempt.rs | 6 ++--- crates/diesel_models/src/payment_attempt.rs | 18 +++++--------- .../src/backend/vir_interpreter/types.rs | 4 ++++ crates/router/src/connector/utils.rs | 3 ++- crates/router/src/core/payments/helpers.rs | 3 ++- .../payments/operations/payment_confirm.rs | 11 +++++++++ .../payments/operations/payment_response.rs | 4 ---- crates/router/src/core/payments/retry.rs | 4 ---- crates/router/src/types.rs | 18 +------------- crates/router/src/workflows/payment_sync.rs | 2 -- .../src/payments/payment_attempt.rs | 24 +++++++------------ .../Payments - Confirm/request.json | 4 ---- .../Payments - Create/request.json | 4 ++++ 15 files changed, 46 insertions(+), 65 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 8710c69aa5c6..dfb8e8999771 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -352,6 +352,9 @@ impl SurchargeDetailsResponse { request_surcharge_details.surcharge_amount == self.surcharge_amount && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_on_surcharge_amount + } } #[derive(Clone, Debug)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index a997960edc7e..74559f8ed69a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -347,6 +347,9 @@ impl RequestSurchargeDetails { final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, } } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_amount.unwrap_or(0) + } } #[derive(Default, Debug, Clone, Copy)] diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index b866237745fb..a937c785902f 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -264,6 +264,8 @@ pub enum PaymentAttemptUpdate { error_message: Option>, amount_capturable: Option, updated_by: String, + surcharge_amount: Option, + tax_amount: Option, merchant_connector_id: Option, }, RejectUpdate { @@ -291,8 +293,6 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -321,8 +321,6 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 42af827f522b..9cc6632c638e 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -180,6 +180,8 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -208,8 +210,6 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -238,8 +238,6 @@ pub enum PaymentAttemptUpdate { error_message: Option>, error_reason: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, unified_code: Option>, unified_message: Option>, @@ -443,6 +441,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, } => Self { amount: Some(amount), currency: Some(currency), @@ -463,6 +463,8 @@ impl From for PaymentAttemptUpdateInternal { amount_capturable, updated_by, merchant_connector_id, + surcharge_amount, + tax_amount, ..Default::default() }, PaymentAttemptUpdate::VoidUpdate { @@ -501,8 +503,6 @@ impl From for PaymentAttemptUpdateInternal { error_reason, connector_response_reference_id, amount_capturable, - surcharge_amount, - tax_amount, updated_by, authentication_data, encoded_data, @@ -524,8 +524,6 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -539,8 +537,6 @@ impl From for PaymentAttemptUpdateInternal { error_message, error_reason, amount_capturable, - surcharge_amount, - tax_amount, updated_by, unified_code, unified_message, @@ -554,8 +550,6 @@ impl From for PaymentAttemptUpdateInternal { error_reason, amount_capturable, updated_by, - surcharge_amount, - tax_amount, unified_code, unified_message, connector_transaction_id, diff --git a/crates/euclid/src/backend/vir_interpreter/types.rs b/crates/euclid/src/backend/vir_interpreter/types.rs index a144cdaafd08..d0eca5fec2ef 100644 --- a/crates/euclid/src/backend/vir_interpreter/types.rs +++ b/crates/euclid/src/backend/vir_interpreter/types.rs @@ -74,6 +74,10 @@ impl Context { } } + if let Some(card_network) = payment_method.card_network { + enum_values.insert(EuclidValue::CardNetwork(card_network)); + } + if let Some(at) = payment.authentication_type { enum_values.insert(EuclidValue::AuthenticationType(at)); } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index e096f1878a9c..803c511f3a6b 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -113,7 +113,8 @@ where enums::AttemptStatus::Charged => { let captured_amount = types::Capturable::get_capture_amount(&self.request, payment_data); - if Some(payment_data.payment_attempt.get_total_amount()) == captured_amount { + let total_capturable_amount = payment_data.payment_attempt.get_total_amount(); + if Some(total_capturable_amount) == captured_amount { enums::AttemptStatus::Charged } else if captured_amount.is_some() { enums::AttemptStatus::PartialCharged diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d813c96ce94b..4b0920a55f51 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1693,7 +1693,8 @@ pub(crate) fn validate_status_with_capture_method( field_name: "payment.status".to_string(), current_flow: "captured".to_string(), current_value: status.to_string(), - states: "requires_capture, partially_captured, processing".to_string() + states: "requires_capture, partially_captured_and_capturable, processing" + .to_string() })) }, ) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 33270795b343..97b0641d2e7e 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -693,6 +693,15 @@ impl let m_error_message = error_message.clone(); let m_db = state.clone().store; + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + let payment_attempt_fut = tokio::spawn( async move { m_db.update_payment_attempt_with_attempt_id( @@ -716,6 +725,8 @@ impl amount_capturable: Some(authorized_amount), updated_by: storage_scheme.to_string(), merchant_connector_id, + surcharge_amount, + tax_amount, }, storage_scheme, ) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index beefa53c56fc..2de5df38dba4 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -372,8 +372,6 @@ async fn payment_response_update_tracker( } else { None }, - surcharge_amount: router_data.request.get_surcharge_amount(), - tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), @@ -497,8 +495,6 @@ async fn payment_response_update_tracker( } else { None }, - surcharge_amount: router_data.request.get_surcharge_amount(), - tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index c5501ab4dc3b..0fd45c5af3b5 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -382,8 +382,6 @@ where } else { None }, - surcharge_amount: None, - tax_amount: None, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, @@ -410,8 +408,6 @@ where status: storage_enums::AttemptStatus::Failure, error_reason: Some(error_response.reason.clone()), amount_capturable: Some(0), - surcharge_amount: None, - tax_amount: None, updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 79c3c11eda14..8c9d030965c9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -551,12 +551,6 @@ pub trait Capturable { { None } - fn get_surcharge_amount(&self) -> Option { - None - } - fn get_tax_on_surcharge_amount(&self) -> Option { - None - } } impl Capturable for PaymentsAuthorizeData { @@ -570,16 +564,6 @@ impl Capturable for PaymentsAuthorizeData { .map(|surcharge_details| surcharge_details.final_amount); final_amount.or(Some(self.amount)) } - fn get_surcharge_amount(&self) -> Option { - self.surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.surcharge_amount) - } - fn get_tax_on_surcharge_amount(&self) -> Option { - self.surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) - } } impl Capturable for PaymentsCaptureData { @@ -620,7 +604,7 @@ impl Capturable for PaymentsSyncData { payment_data .payment_attempt .amount_to_capture - .or(Some(payment_data.payment_attempt.get_total_amount())) + .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 04f91f30bc7e..f2760a00582d 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -135,8 +135,6 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { consts::REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC.to_string(), )), amount_capturable: Some(0), - surcharge_amount: None, - tax_amount: None, updated_by: merchant_account.storage_scheme.to_string(), unified_code: None, unified_message: None, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 0526fcec9c53..543cf1059889 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1215,6 +1215,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1234,6 +1236,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1261,8 +1265,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1282,8 +1284,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1320,8 +1320,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, - tax_amount, - surcharge_amount, updated_by, unified_code, unified_message, @@ -1333,8 +1331,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, - surcharge_amount, - tax_amount, updated_by, unified_code, unified_message, @@ -1480,6 +1476,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1499,6 +1497,8 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1526,8 +1526,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1547,8 +1545,6 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, - surcharge_amount, - tax_amount, authentication_data, encoded_data, unified_code, @@ -1585,8 +1581,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_message, error_reason, amount_capturable, - surcharge_amount, - tax_amount, updated_by, unified_code, unified_message, @@ -1599,8 +1593,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_reason, amount_capturable, updated_by, - surcharge_amount, - tax_amount, unified_code, unified_message, connector_transaction_id, diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json index 8559af25e82c..91426564e8e1 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Confirm/request.json @@ -39,10 +39,6 @@ }, "raw_json_formatted": { "client_secret": "{{client_secret}}", - "surcharge_details": { - "surcharge_amount": 5, - "tax_amount": 5 - }, "payment_method": "card", "payment_method_data": { "card": { diff --git a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json index f7d813c34efd..9e084a35c8c9 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json +++ b/postman/collection-dir/paypal/Flow Testcases/Happy Cases/Scenario8-Create payment with Manual capture with confirm false and surcharge_data/Payments - Create/request.json @@ -31,6 +31,10 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", + "surcharge_details": { + "surcharge_amount": 5, + "tax_amount": 5 + }, "billing": { "address": { "line1": "1467", From 4c1c6da0d1b3e4145f0bc38b06af2d2a1d643232 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde Date: Fri, 24 Nov 2023 19:13:07 +0530 Subject: [PATCH 071/443] chore(version): v1.89.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e427f33e8fbf..d6197598e564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.89.0 (2023-11-24) + +### Features + +- **router:** Add `connector_transaction_id` in error_response from connector flows ([#2972](https://github.com/juspay/hyperswitch/pull/2972)) ([`3322103`](https://github.com/juspay/hyperswitch/commit/3322103f5c9b7c2a5b663980246c6ca36b8dc63e)) + +### Bug Fixes + +- **connector:** [BANKOFAMERICA] Add status VOIDED in enum Bankofameri… ([#2969](https://github.com/juspay/hyperswitch/pull/2969)) ([`203bbd7`](https://github.com/juspay/hyperswitch/commit/203bbd73751e1513206e81d7cf920ec263f83c58)) +- **core:** Error propagation for not supporting partial refund ([#2976](https://github.com/juspay/hyperswitch/pull/2976)) ([`97a38a7`](https://github.com/juspay/hyperswitch/commit/97a38a78e514e4fa3b5db46b6de985be6312dcc3)) +- **router:** Mark refund status as failure for not_implemented error from connector flows ([#2978](https://github.com/juspay/hyperswitch/pull/2978)) ([`d56d805`](https://github.com/juspay/hyperswitch/commit/d56d80557050336d5ed37282f1aa34b6c17389d1)) +- Return none instead of err when payment method data is not found for bank debit during listing ([#2967](https://github.com/juspay/hyperswitch/pull/2967)) ([`5cc829a`](https://github.com/juspay/hyperswitch/commit/5cc829a11f515a413fe19f657a90aa05cebb99b5)) +- Surcharge related status and rules fix ([#2974](https://github.com/juspay/hyperswitch/pull/2974)) ([`3db7213`](https://github.com/juspay/hyperswitch/commit/3db721388a7f0e291d7eb186661fc69a57068ea6)) + +### Documentation + +- **README:** Updated Community Platform Mentions ([#2960](https://github.com/juspay/hyperswitch/pull/2960)) ([`e0bde43`](https://github.com/juspay/hyperswitch/commit/e0bde433282a34eb9eb28a2d9c43c2b17b5e65e5)) +- Add Rust locker information in architecture doc ([#2964](https://github.com/juspay/hyperswitch/pull/2964)) ([`b2f7dd1`](https://github.com/juspay/hyperswitch/commit/b2f7dd13925a1429e316cd9eaf0e2d31d46b6d4a)) + +**Full Changelog:** [`v1.88.0...v1.89.0`](https://github.com/juspay/hyperswitch/compare/v1.88.0...v1.89.0) + +- - - + + ## 1.88.0 (2023-11-23) ### Features From 03c0a772a99000acf4676db8ca2ce916036281d1 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:11:46 +0530 Subject: [PATCH 072/443] feat(auth): Add Authorization for JWT Authentication types (#2973) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/analytics/routes.rs | 29 ++- crates/router/src/consts.rs | 2 + crates/router/src/consts/user.rs | 6 - crates/router/src/core/user.rs | 4 +- crates/router/src/routes/admin.rs | 37 +++- crates/router/src/routes/api_keys.rs | 10 +- crates/router/src/routes/disputes.rs | 38 +++- crates/router/src/routes/files.rs | 20 ++- crates/router/src/routes/mandates.rs | 8 +- crates/router/src/routes/payment_link.rs | 2 +- crates/router/src/routes/payments.rs | 31 +++- crates/router/src/routes/refunds.rs | 26 ++- crates/router/src/routes/routing.rs | 158 ++++++++++++---- crates/router/src/routes/verification.rs | 14 +- crates/router/src/services.rs | 1 + crates/router/src/services/authentication.rs | 16 +- crates/router/src/services/authorization.rs | 27 +++ .../router/src/services/authorization/info.rs | 168 ++++++++++++++++++ .../src/services/authorization/permissions.rs | 74 ++++++++ .../authorization/predefined_permissions.rs | 79 ++++++++ 20 files changed, 659 insertions(+), 91 deletions(-) create mode 100644 crates/router/src/services/authorization.rs create mode 100644 crates/router/src/services/authorization/info.rs create mode 100644 crates/router/src/services/authorization/permissions.rs create mode 100644 crates/router/src/services/authorization/predefined_permissions.rs diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs index 298ec61ec903..113312cdf10f 100644 --- a/crates/router/src/analytics/routes.rs +++ b/crates/router/src/analytics/routes.rs @@ -8,7 +8,10 @@ use router_env::AnalyticsFlow; use super::{core::*, payments, refunds, types::AnalyticsDomain}; use crate::{ core::api_locking, - services::{api, authentication as auth, authentication::AuthenticationData}, + services::{ + api, authentication as auth, authentication::AuthenticationData, + authorization::permissions::Permission, + }, AppState, }; @@ -68,7 +71,11 @@ pub async fn get_payment_metrics( |state, auth: AuthenticationData, req| { payments::get_metrics(state.pool.clone(), auth.merchant_account, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -98,7 +105,11 @@ pub async fn get_refunds_metrics( |state, auth: AuthenticationData, req| { refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -118,7 +129,11 @@ pub async fn get_payment_filters( |state, auth: AuthenticationData, req| { payment_filters_core(state.pool.clone(), req, auth.merchant_account) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -138,7 +153,11 @@ pub async fn get_refund_filters( |state, auth: AuthenticationData, req: GetRefundFilterRequest| { refund_filter_core(state.pool.clone(), req, auth.merchant_account) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::Analytics), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 410e3c1113b1..c5490ee00e63 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -58,3 +58,5 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes #[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days + +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index 3a71fed01a12..c570aca76038 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -1,8 +1,2 @@ -#[cfg(feature = "olap")] pub const MAX_NAME_LENGTH: usize = 70; -#[cfg(feature = "olap")] pub const MAX_COMPANY_NAME_LENGTH: usize = 70; - -// USER ROLES -#[cfg(any(feature = "olap", feature = "oltp"))] -pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 710dc9281bfa..8b4cf45fe5ef 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -5,9 +5,7 @@ use masking::{ExposeInterface, Secret}; use router_env::env; use super::errors::{UserErrors, UserResponse}; -use crate::{ - consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain, -}; +use crate::{consts, routes::AppState, services::ApplicationResponse, types::domain}; pub async fn connect_account( state: AppState, diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index eef8cacc5f92..0586faabbf76 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{admin::*, api_locking}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::admin, }; @@ -77,7 +77,10 @@ pub async fn retrieve_merchant_account( |state, _, req| get_merchant_account(state, req), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -141,6 +144,7 @@ pub async fn update_merchant_account( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -220,6 +224,7 @@ pub async fn payment_connector_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), @@ -270,7 +275,10 @@ pub async fn payment_connector_retrieve( }, auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -312,7 +320,10 @@ pub async fn payment_connector_list( |state, _, merchant_id| list_payment_connectors(state, merchant_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -359,6 +370,7 @@ pub async fn payment_connector_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantConnectorAccountWrite, }, req.headers(), ), @@ -407,7 +419,10 @@ pub async fn payment_connector_delete( |state, _, req| delete_payment_connector(state, req.merchant_id, req.merchant_connector_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantConnectorAccountWrite, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -460,6 +475,7 @@ pub async fn business_profile_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -484,7 +500,10 @@ pub async fn business_profile_retrieve( |state, _, profile_id| retrieve_business_profile(state, profile_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, @@ -511,6 +530,7 @@ pub async fn business_profile_update( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::MerchantAccountWrite, }, req.headers(), ), @@ -555,7 +575,10 @@ pub async fn business_profiles_list( |state, _, merchant_id| list_business_profile(state, merchant_id), auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::MerchantAccountRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 7299aa696390..5b4c047b1466 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_keys, api_locking}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api as api_types, }; @@ -57,6 +57,7 @@ pub async fn api_key_create( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -101,6 +102,7 @@ pub async fn api_key_retrieve( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyRead, }, req.headers(), ), @@ -189,6 +191,7 @@ pub async fn api_key_revoke( &auth::AdminApiAuth, &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), + required_permission: Permission::ApiKeyWrite, }, req.headers(), ), @@ -237,7 +240,10 @@ pub async fn api_key_list( }, auth::auth_type( &auth::AdminApiAuth, - &auth::JWTAuthMerchantFromRoute { merchant_id }, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::ApiKeyRead, + }, req.headers(), ), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index aaeb118645db..7bcd8ad35124 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -3,7 +3,7 @@ use actix_web::{web, HttpRequest, HttpResponse}; use api_models::disputes as dispute_models; use router_env::{instrument, tracing, Flow}; -use crate::core::api_locking; +use crate::{core::api_locking, services::authorization::permissions::Permission}; pub mod utils; use super::app::AppState; @@ -44,7 +44,11 @@ pub async fn retrieve_dispute( &req, dispute_id, |state, auth, req| disputes::retrieve_dispute(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -87,7 +91,11 @@ pub async fn retrieve_disputes_list( &req, payload, |state, auth, req| disputes::retrieve_disputes_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -125,7 +133,11 @@ pub async fn accept_dispute( |state, auth, req| { disputes::accept_dispute(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -158,7 +170,11 @@ pub async fn submit_dispute_evidence( |state, auth, req| { disputes::submit_evidence(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -199,7 +215,11 @@ pub async fn attach_dispute_evidence( |state, auth, req| { disputes::attach_evidence(state, auth.merchant_account, auth.key_store, req) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -235,7 +255,11 @@ pub async fn retrieve_dispute_evidence( &req, dispute_id, |state, auth, req| disputes::retrieve_dispute_evidence(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::DisputeRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index bde221ebc161..95f4007cb91b 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -2,7 +2,7 @@ use actix_multipart::Multipart; use actix_web::{web, HttpRequest, HttpResponse}; use router_env::{instrument, tracing, Flow}; -use crate::core::api_locking; +use crate::{core::api_locking, services::authorization::permissions::Permission}; pub mod transformers; use super::app::AppState; @@ -45,7 +45,11 @@ pub async fn files_create( &req, create_file_request, |state, auth, req| files_create_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -83,7 +87,11 @@ pub async fn files_delete( &req, file_id, |state, auth, req| files_delete_core(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -121,7 +129,11 @@ pub async fn files_retrieve( &req, file_id, |state, auth, req| files_retrieve_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::FileRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index 0213d48ddca7..1e4461362975 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, mandate}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::mandates, }; @@ -122,7 +122,11 @@ pub async fn retrieve_mandates_list( &req, payload, |state, auth, req| mandate::retrieve_mandates_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MandateRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 4c26ea71f7d5..d45d67568b89 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -118,7 +118,7 @@ pub async fn payments_link_list( &req, payload, |state, auth, payload| list_payment_link(state, auth.merchant_account, payload), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 81e53ade5e96..979b15a3d7f2 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1,4 +1,7 @@ -use crate::core::api_locking::{self, GetLockingInput}; +use crate::{ + core::api_locking::{self, GetLockingInput}, + services::authorization::permissions::Permission, +}; pub mod helpers; use actix_web::{web, Responder}; @@ -128,7 +131,11 @@ pub async fn payments_create( }, match env::which() { env::Env::Production => &auth::ApiKeyAuth, - _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + req.headers(), + ), }, locking_action, )) @@ -262,7 +269,7 @@ pub async fn payments_retrieve( }, auth::auth_type( &*auth_type, - &auth::JWTAuth, + &auth::JWTAuth(Permission::PaymentRead), req.headers(), ), locking_action, @@ -843,7 +850,11 @@ pub async fn payments_list( &req, payload, |state, auth, req| payments::list_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -863,7 +874,11 @@ pub async fn payments_list_by_filter( &req, payload, |state, auth, req| payments::apply_filters_on_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -883,7 +898,11 @@ pub async fn get_filters_for_payments( &req, payload, |state, auth, req| payments::get_filters_for_payments(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index d370af6b8d7a..47e9f2bf42a8 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, refunds::*}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::refunds, }; @@ -37,7 +37,11 @@ pub async fn refunds_create( &req, json_payload.into_inner(), |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -88,7 +92,11 @@ pub async fn refunds_retrieve( refund_retrieve_core, ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -202,7 +210,11 @@ pub async fn refunds_list( &req, payload.into_inner(), |state, auth, req| refund_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -235,7 +247,11 @@ pub async fn refunds_filter_list( &req, payload.into_inner(), |state, auth, req| refund_filter_list(state, auth.merchant_account, req), - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RefundRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 1d2549bb047a..e7e31cb36aeb 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -14,7 +14,7 @@ use router_env::{ use crate::{ core::{api_locking, conditional_config, routing, surcharge_decision_config}, routes::AppState, - services::{api as oss_api, authentication as auth}, + services::{api as oss_api, authentication as auth, authorization::permissions::Permission}, }; #[cfg(feature = "olap")] @@ -34,9 +34,13 @@ pub async fn routing_create_config( routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -65,9 +69,13 @@ pub async fn routing_link_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -91,9 +99,13 @@ pub async fn routing_retrieve_config( routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -122,9 +134,13 @@ pub async fn routing_retrieve_dictionary( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -142,9 +158,13 @@ pub async fn routing_retrieve_dictionary( routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -172,9 +192,13 @@ pub async fn routing_unlink_config( routing::unlink_routing_config(state, auth.merchant_account, payload_req) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -192,9 +216,13 @@ pub async fn routing_unlink_config( routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, )) .await @@ -217,9 +245,13 @@ pub async fn routing_update_default_config( routing::update_default_routing_config(state, auth.merchant_account, updated_config) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, ) .await @@ -240,9 +272,13 @@ pub async fn routing_retrieve_default_config( routing::retrieve_default_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, ) .await @@ -270,9 +306,13 @@ pub async fn upsert_surcharge_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), api_locking::LockAction::NotApplicable, )) .await @@ -297,9 +337,13 @@ pub async fn delete_surcharge_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), api_locking::LockAction::NotApplicable, )) .await @@ -324,9 +368,13 @@ pub async fn retrieve_surcharge_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, ) .await @@ -354,9 +402,13 @@ pub async fn upsert_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, )) .await @@ -382,9 +434,13 @@ pub async fn delete_decision_manager_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), api_locking::LockAction::NotApplicable, )) .await @@ -406,9 +462,13 @@ pub async fn retrieve_decision_manager_config( conditional_config::retrieve_conditional_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), api_locking::LockAction::NotApplicable, ) .await @@ -434,9 +494,13 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -454,9 +518,13 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingRead), api_locking::LockAction::NotApplicable, )) .await @@ -478,9 +546,17 @@ pub async fn routing_retrieve_default_config_for_profiles( routing::retrieve_default_routing_config_for_profiles(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -512,9 +588,13 @@ pub async fn routing_update_default_config_for_profile( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::RoutingWrite), + req.headers(), + ), #[cfg(feature = "release")] - &auth::JWTAuth, + &auth::JWTAuth(Permission::RoutingWrite), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index d0525bb272e8..4bcbacdf9912 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -5,7 +5,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, verification}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, }; #[instrument(skip_all, fields(flow = ?Flow::Verification))] @@ -32,7 +32,11 @@ pub async fn apple_pay_merchant_registration( merchant_id.clone(), ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -60,7 +64,11 @@ pub async fn retrieve_apple_pay_verified_domains( mca_id.to_string(), ) }, - auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 21f33f0fa0b8..2d5552b59d17 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,5 +1,6 @@ pub mod api; pub mod authentication; +pub mod authorization; pub mod encryption; #[cfg(feature = "olap")] pub mod jwt; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 4277205b0231..876804b7bb93 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -9,6 +9,7 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; +use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; #[cfg(feature = "olap")] @@ -387,7 +388,7 @@ where } #[derive(Debug)] -pub(crate) struct JWTAuth; +pub(crate) struct JWTAuth(pub Permission); #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchUnit { @@ -406,6 +407,10 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + Ok(( (), AuthenticationType::MerchantJWT { @@ -418,6 +423,7 @@ where pub struct JWTAuthMerchantFromRoute { pub merchant_id: String, + pub required_permission: Permission, } #[async_trait] @@ -432,6 +438,9 @@ where ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.required_permission, permissions)?; + // Check if token has access to merchantID that has been requested through query param if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); @@ -460,6 +469,7 @@ where #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchMerchantAccount { merchant_id: String, + role_id: String, } #[async_trait] @@ -475,6 +485,10 @@ where let payload = parse_jwt_payload::(request_headers, state) .await?; + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + let key_store = state .store() .get_merchant_key_store_by_merchant_id( diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs new file mode 100644 index 000000000000..cad9b1ece62e --- /dev/null +++ b/crates/router/src/services/authorization.rs @@ -0,0 +1,27 @@ +use crate::core::errors::{ApiErrorResponse, RouterResult}; + +pub mod info; +pub mod permissions; +pub mod predefined_permissions; + +pub fn get_permissions(role: &str) -> RouterResult<&Vec> { + predefined_permissions::PREDEFINED_PERMISSIONS + .get(role) + .map(|role_info| role_info.get_permissions()) + .ok_or(ApiErrorResponse::InvalidJwtToken.into()) +} + +pub fn check_authorization( + required_permission: &permissions::Permission, + permissions: &[permissions::Permission], +) -> RouterResult<()> { + permissions + .contains(required_permission) + .then_some(()) + .ok_or( + ApiErrorResponse::AccessForbidden { + resource: required_permission.to_string(), + } + .into(), + ) +} diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs new file mode 100644 index 000000000000..c6b649f3de5c --- /dev/null +++ b/crates/router/src/services/authorization/info.rs @@ -0,0 +1,168 @@ +use strum::{EnumIter, IntoEnumIterator}; + +use super::permissions::Permission; + +pub fn get_authorization_info() -> Vec { + PermissionModule::iter() + .map(|module| ModuleInfo::new(&module)) + .collect() +} + +pub struct PermissionInfo { + pub enum_name: Permission, + pub description: &'static str, +} + +impl PermissionInfo { + pub fn new(permissions: &[Permission]) -> Vec { + let mut permission_infos = Vec::with_capacity(permissions.len()); + for permission in permissions { + if let Some(description) = Permission::get_permission_description(permission) { + permission_infos.push(Self { + enum_name: permission.clone(), + description, + }) + } + } + permission_infos + } +} + +#[derive(PartialEq, EnumIter, Clone)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Connectors, + Forex, + Routing, + Analytics, + Mandates, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, +} + +impl PermissionModule { + pub fn get_module_description(&self) -> &'static str { + match self { + Self::Payments => "Everything related to payments - like creating and viewing payment related information are within this module", + Self::Refunds => "Refunds module encompasses everything related to refunds - like creating and viewing payment related information", + Self::MerchantAccount => "Accounts module permissions allow the user to view and update account details, configure webhooks and much more", + Self::Connectors => "All connector related actions - like configuring new connectors, viewing and updating connector configuration lies with this module", + Self::Routing => "All actions related to new, active, and past routing stacks take place here", + Self::Forex => "Forex module permissions allow the user to view and query the forex rates", + Self::Analytics => "Permission to view and analyse the data relating to payments, refunds, sdk etc.", + Self::Mandates => "Everything related to mandates - like creating and viewing mandate related information are within this module", + Self::Disputes => "Everything related to disputes - like creating and viewing dispute related information are within this module", + Self::Files => "Permissions for uploading, deleting and viewing files for disputes", + Self::ThreeDsDecisionManager => "View and configure 3DS decision rules configured for a merchant", + Self::SurchargeDecisionManager =>"View and configure surcharge decision rules configured for a merchant" + } + } +} + +pub struct ModuleInfo { + pub module: PermissionModule, + pub description: &'static str, + pub permissions: Vec, +} + +impl ModuleInfo { + pub fn new(module: &PermissionModule) -> Self { + let module_name = module.clone(); + let description = module.get_module_description(); + + match module { + PermissionModule::Payments => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::PaymentRead, + Permission::PaymentWrite, + ]), + }, + PermissionModule::Refunds => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::RefundRead, + Permission::RefundWrite, + ]), + }, + PermissionModule::MerchantAccount => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + ]), + }, + PermissionModule::Connectors => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + ]), + }, + PermissionModule::Forex => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::ForexRead]), + }, + PermissionModule::Routing => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::RoutingRead, + Permission::RoutingWrite, + ]), + }, + PermissionModule::Analytics => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::Analytics]), + }, + PermissionModule::Mandates => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::MandateRead, + Permission::MandateWrite, + ]), + }, + PermissionModule::Disputes => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::DisputeRead, + Permission::DisputeWrite, + ]), + }, + PermissionModule::Files => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::FileRead, Permission::FileWrite]), + }, + PermissionModule::ThreeDsDecisionManager => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + ]), + }, + + PermissionModule::SurchargeDecisionManager => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + ]), + }, + } + } +} diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs new file mode 100644 index 000000000000..708da97e1e39 --- /dev/null +++ b/crates/router/src/services/authorization/permissions.rs @@ -0,0 +1,74 @@ +use strum::Display; + +#[derive(PartialEq, Display, Clone, Debug)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, + MerchantAccountCreate, +} + +impl Permission { + pub fn get_permission_description(&self) -> Option<&'static str> { + match self { + Self::PaymentRead => Some("View all payments"), + Self::PaymentWrite => Some("Create payment, download payments data"), + Self::RefundRead => Some("View all refunds"), + Self::RefundWrite => Some("Create refund, download refunds data"), + Self::ApiKeyRead => Some("View API keys (masked generated for the system"), + Self::ApiKeyWrite => Some("Create and update API keys"), + Self::MerchantAccountRead => Some("View merchant account details"), + Self::MerchantAccountWrite => { + Some("Update merchant account details, configure webhooks, manage api keys") + } + Self::MerchantConnectorAccountRead => Some("View connectors configured"), + Self::MerchantConnectorAccountWrite => { + Some("Create, update, verify and delete connector configurations") + } + Self::ForexRead => Some("Query Forex data"), + Self::RoutingRead => Some("View routing configuration"), + Self::RoutingWrite => Some("Create and activate routing configurations"), + Self::DisputeRead => Some("View disputes"), + Self::DisputeWrite => Some("Create and update disputes"), + Self::MandateRead => Some("View mandates"), + Self::MandateWrite => Some("Create and update mandates"), + Self::FileRead => Some("View files"), + Self::FileWrite => Some("Create, update and delete files"), + Self::Analytics => Some("Access to analytics module"), + Self::ThreeDsDecisionManagerWrite => Some("Create and update 3DS decision rules"), + Self::ThreeDsDecisionManagerRead => { + Some("View all 3DS decision rules configured for a merchant") + } + Self::SurchargeDecisionManagerWrite => { + Some("Create and update the surcharge decision rules") + } + Self::SurchargeDecisionManagerRead => Some("View all the surcharge decision rules"), + Self::UsersRead => Some("View all the users for a merchant"), + Self::UsersWrite => Some("Invite users, assign and update roles"), + Self::MerchantAccountCreate => None, + } + } +} diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs new file mode 100644 index 000000000000..89fa2c8f739c --- /dev/null +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; + +use once_cell::sync::Lazy; + +use super::permissions::Permission; +use crate::consts; + +pub struct RoleInfo { + permissions: Vec, + name: Option<&'static str>, + is_invitable: bool, +} + +impl RoleInfo { + pub fn get_permissions(&self) -> &Vec { + &self.permissions + } + + pub fn get_name(&self) -> Option<&'static str> { + self.name + } + + pub fn is_invitable(&self) -> bool { + self.is_invitable + } +} + +pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy::new(|| { + let mut roles = HashMap::new(); + roles.insert( + consts::ROLE_ID_ORGANIZATION_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: Some("Organization Admin"), + is_invitable: false, + }, + ); + roles +}); + +pub fn get_role_name_from_id(role_id: &str) -> Option<&'static str> { + PREDEFINED_PERMISSIONS + .get(role_id) + .and_then(|role_info| role_info.name) +} + +pub fn is_role_invitable(role_id: &str) -> bool { + PREDEFINED_PERMISSIONS + .get(role_id) + .map_or(false, |role_info| role_info.is_invitable) +} From 107c3b99417dd7bca7b62741ad601485700f37be Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Fri, 24 Nov 2023 19:25:19 +0530 Subject: [PATCH 073/443] fix: add prefix to connector_transaction_id (#2981) --- crates/storage_impl/src/payments/payment_attempt.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 543cf1059889..06aacccc769d 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -553,7 +553,7 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); + let lookup_id = format!("conn_trans_{merchant_id}_{connector_transaction_id}"); let lookup = self .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; @@ -774,7 +774,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{preprocessing_id}"); + let lookup_id = format!("preprocessing_{merchant_id}_{preprocessing_id}"); let lookup = self .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; @@ -1671,7 +1671,7 @@ async fn add_connector_txn_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("{}_{}", merchant_id, connector_transaction_id), + lookup_id: format!("conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1693,7 +1693,7 @@ async fn add_preprocessing_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("{}_{}", merchant_id, preprocessing_id), + lookup_id: format!("preprocessing_{}_{}", merchant_id, preprocessing_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), From bfa1645b847fb881eb2370d5dbfef6fd0b53725d Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:34:27 +0530 Subject: [PATCH 074/443] feat(user): implement change password for user (#2959) --- crates/api_models/src/events/user.rs | 4 +- crates/api_models/src/user.rs | 6 ++ crates/router/src/core/errors/user.rs | 13 +++ crates/router/src/core/user.rs | 42 +++++++++- crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 2 +- crates/router/src/routes/user.rs | 18 ++++ crates/router/src/services/authentication.rs | 87 +++++++++++++++++++- crates/router_env/src/logger/types.rs | 2 + 9 files changed, 170 insertions(+), 5 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 2a896cc38776..4e9f2f284173 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,6 +1,6 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; +use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse}; impl ApiEventMetric for ConnectAccountResponse { fn get_api_event_type(&self) -> Option { @@ -12,3 +12,5 @@ impl ApiEventMetric for ConnectAccountResponse { } impl ApiEventMetric for ConnectAccountRequest {} + +common_utils::impl_misc_api_event_type!(ChangePasswordRequest); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 91f7702c654e..41ea9cc5193a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -19,3 +19,9 @@ pub struct ConnectAccountResponse { #[serde(skip_serializing)] pub user_id: String, } + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ChangePasswordRequest { + pub new_password: Secret, + pub old_password: Secret, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b4d48365dc84..b86c395b9814 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -13,6 +13,8 @@ pub enum UserErrors { InvalidCredentials, #[error("UserExists")] UserExists, + #[error("InvalidOldPassword")] + InvalidOldPassword, #[error("EmailParsingError")] EmailParsingError, #[error("NameParsingError")] @@ -27,6 +29,8 @@ pub enum UserErrors { InvalidEmailError, #[error("DuplicateOrganizationId")] DuplicateOrganizationId, + #[error("MerchantIdNotFound")] + MerchantIdNotFound, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -49,6 +53,12 @@ impl common_utils::errors::ErrorSwitch AER::BadRequest(ApiError::new( + sub_code, + 6, + "Old password incorrect. Please enter the correct password", + None, + )), Self::EmailParsingError => { AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) } @@ -73,6 +83,9 @@ impl common_utils::errors::ErrorSwitch { + AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 8b4cf45fe5ef..94cd482a2291 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,11 +1,17 @@ use api_models::user as api; use diesel_models::enums::UserStatus; -use error_stack::IntoReport; +use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use router_env::env; use super::errors::{UserErrors, UserResponse}; -use crate::{consts, routes::AppState, services::ApplicationResponse, types::domain}; +use crate::{ + consts, + db::user::UserInterface, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + types::domain, +}; pub async fn connect_account( state: AppState, @@ -77,3 +83,35 @@ pub async fn connect_account( Err(UserErrors::InternalServerError.into()) } } + +pub async fn change_password( + state: AppState, + request: api::ChangePasswordRequest, + user_from_token: UserFromToken, +) -> UserResponse<()> { + let user: domain::UserFromStorage = + UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + user.compare_password(request.old_password) + .change_context(UserErrors::InvalidOldPassword)?; + + let new_password_hash = + crate::utils::user::password::generate_password_hash(request.new_password)?; + + let _ = UserInterface::update_user_by_user_id( + &*state.store, + user.get_user_id(), + diesel_models::user::UserUpdate::AccountUpdate { + name: None, + password: Some(new_password_hash), + is_verified: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 96bb47ea4e97..84848e030120 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -759,6 +759,7 @@ impl User { .service(web::resource("/signup").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/change_password").route(web::post().to(change_password))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index a9cf7b44a73d..219948bdd4d2 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -144,7 +144,7 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount => Self::User, + Flow::UserConnectAccount | Flow::ChangePassword => Self::User, } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 0ff11ce087b5..7d3d183eda76 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -29,3 +29,21 @@ pub async fn user_connect_account( )) .await } + +pub async fn change_password( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ChangePassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, user, req| user::change_password(state, req, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 876804b7bb93..e24c7cebcb2a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -14,6 +14,8 @@ use super::authorization::{self, permissions::Permission}; use super::jwt; #[cfg(feature = "olap")] use crate::consts; +#[cfg(feature = "olap")] +use crate::core::errors::UserResult; use crate::{ configs::settings, core::{ @@ -97,7 +99,7 @@ impl AuthToken { role_id: String, settings: &settings::Settings, org_id: String, - ) -> errors::UserResult { + ) -> UserResult { let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(exp_duration)?.as_secs(); let token_payload = Self { @@ -111,6 +113,14 @@ impl AuthToken { } } +#[derive(Clone)] +pub struct UserFromToken { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub org_id: String, +} + pub trait AuthInfo { fn get_merchant_id(&self) -> Option<&str>; } @@ -421,6 +431,34 @@ where } } +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + }, + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + pub struct JWTAuthMerchantFromRoute { pub merchant_id: String, pub required_permission: Permission, @@ -519,6 +557,53 @@ where } } +pub struct DashboardNoPermissionAuth; + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + UserFromToken { + user_id: payload.user_id.clone(), + merchant_id: payload.merchant_id.clone(), + org_id: payload.org_id, + role_id: payload.role_id, + }, + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch<(), A> for DashboardNoPermissionAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + parse_jwt_payload::(request_headers, state).await?; + + Ok(((), AuthenticationType::NoAuth)) + } +} + pub trait ClientSecretFetch { fn get_client_secret(&self) -> Option<&String>; } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 178f837fce18..7978e98e52c0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -255,6 +255,8 @@ pub enum Flow { DecisionManagerDeleteConfig, /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, + /// Change password flow + ChangePassword, } /// From 04b7c0384dc9290bd60f49033fd35732527720f1 Mon Sep 17 00:00:00 2001 From: HeetVekariya <91054457+HeetVekariya@users.noreply.github.com> Date: Mon, 27 Nov 2023 01:33:24 +0530 Subject: [PATCH 075/443] refactor(connector): [Nuvei] update error message (#2867) --- crates/router/src/connector/nuvei/transformers.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 25562f54bfeb..b79b2c892643 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -623,11 +623,9 @@ impl TryFrom for NuveiBIC { | api_models::enums::BankNames::TsbBank | api_models::enums::BankNames::TescoBank | api_models::enums::BankNames::UlsterBank => { - Err(errors::ConnectorError::NotSupported { - message: bank.to_string(), - connector: "Nuvei", - } - .into()) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nuvei"), + ))? } } } @@ -693,10 +691,9 @@ impl bank_name.map(NuveiBIC::try_from).transpose()?, ) } - _ => Err(errors::ConnectorError::NotSupported { - message: "Bank Redirect".to_string(), - connector: "Nuvei", - })?, + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nuvei"), + ))?, }; Ok(Self { payment_option: PaymentOption { From 37532d46f599a99e0e021b0455a6f02381005dd7 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Mon, 27 Nov 2023 10:43:59 +0530 Subject: [PATCH 076/443] fix(router): added validation to check total orderDetails amount equal to amount in request (#2965) Co-authored-by: Sahkal Poddar --- crates/router/src/core/payments/helpers.rs | 19 +++++++++++++++++++ .../payments/operations/payment_confirm.rs | 7 +++++++ .../payments/operations/payment_create.rs | 7 +++++++ .../payments/operations/payment_update.rs | 7 +++++++ 4 files changed, 40 insertions(+) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4b0920a55f51..f57c0640f1a8 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3686,3 +3686,22 @@ pub async fn get_gsm_record( }) .ok() } + +pub fn validate_order_details_amount( + order_details: Vec, + amount: i64, +) -> Result<(), errors::ApiErrorResponse> { + let total_order_details_amount: i64 = order_details + .iter() + .map(|order| order.amount * i64::from(order.quantity)) + .sum(); + + if total_order_details_amount != amount { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Total sum of order details doesn't match amount in payment request" + .to_string(), + }) + } else { + Ok(()) + } +} diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 97b0641d2e7e..28b6dbec96ab 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -102,6 +102,13 @@ impl utils::flatten_join_error(mandate_details_fut) )?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + helpers::validate_customer_access(&payment_intent, auth_flow, request)?; helpers::validate_payment_status_against_not_allowed_statuses( diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index ccf9fc3fad1c..c12f28e23390 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -186,6 +186,13 @@ impl payment_id: payment_id.clone(), })?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + payment_attempt = db .insert_payment_attempt(payment_attempt_new, storage_scheme) .await diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 75d3b6b82b4c..1176eeb1dd3f 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -60,6 +60,13 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + if let Some(order_details) = &request.order_details { + helpers::validate_order_details_amount( + order_details.to_owned(), + payment_intent.amount, + )?; + } + payment_intent.setup_future_usage = request .setup_future_usage .or(payment_intent.setup_future_usage); From 34953a046429fe0341e8469bd9b036e176bda205 Mon Sep 17 00:00:00 2001 From: Jagan Date: Mon, 27 Nov 2023 14:06:30 +0530 Subject: [PATCH 077/443] chore(connector): update connector addition script (#2801) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- connector-template/test.rs | 1 + scripts/add_connector.sh | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/connector-template/test.rs b/connector-template/test.rs index 5bbf761dea19..7b093ddb6efa 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -17,6 +17,7 @@ impl utils::Connector for {{project-name | downcase | pascal_case}}Test { connector: Box::new(&{{project-name | downcase | pascal_case}}), connector_name: types::Connector::{{project-name | downcase | pascal_case}}, get_token: types::api::GetToken::Connector, + merchant_connector_id: None, } } diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9fdc57bf3c81..9a30fe9d7573 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -45,7 +45,7 @@ cd $SCRIPT/.. # Remove template files if already created for this connector rm -rf $conn/$payment_gateway $conn/$payment_gateway.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs $src/core/payments/flows.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs $src/core/admin.rs $src/core/payments/routing/transformers.rs $src/types/transformers.rs # Add enum for this connector in required places previous_connector='' @@ -54,15 +54,21 @@ previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connec sed -i'' -e "s|pub mod $previous_connector;|pub mod $previous_connector;\npub mod ${payment_gateway};|" $conn.rs sed -i'' -e "s/};/${payment_gateway}::${payment_gateway_camelcase},\n};/" $conn.rs sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tenums::Connector::${payment_gateway_camelcase} => Ok(Box::new(\&connector::${payment_gateway_camelcase})),|" $src/types/api.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tRoutableConnectors::${payment_gateway_camelcase} => euclid_enums::Connector::${payment_gateway_camelcase},|" crates/api_models/src/routing.rs sed -i'' -e "s/pub $previous_connector: \(.*\)/pub $previous_connector: \1\n\tpub ${payment_gateway}: ConnectorParams,/" $src/configs/settings.rs sed -i'' -e "s|$previous_connector.base_url \(.*\)|$previous_connector.base_url \1\n${payment_gateway}.base_url = \"$base_url\"|" config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml sed -r -i'' -e "s/\"$previous_connector\",/\"$previous_connector\",\n \"${payment_gateway}\",/" config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs +sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/euclid/src/enums.rs +sed -i '' -e "s/\(match connector_name {\)/\1\n\t\tapi_enums::Connector::${payment_gateway_camelcase} => {${payment_gateway}::transformers::${payment_gateway_camelcase}AuthType::try_from(val)?;Ok(())}/" $src/core/admin.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::RoutableConnectors::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/core/payments/routing/transformers.rs +sed -i'' -e "s|dsl_enums::Connector::$previous_connector_camelcase \(.*\)|dsl_enums::Connector::$previous_connector_camelcase \1\n\t\t\tdsl_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs +sed -i'' -e "s|api_enums::Connector::$previous_connector_camelcase \(.*\)|api_enums::Connector::$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs sed -i'' -e "s/^default_imp_for_\(.*\)/default_imp_for_\1\n\tconnector::${payment_gateway_camelcase},/" $src/core/payments/flows.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 config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e $src/core/payments/flows.rs-e +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e $src/core/admin.rs-e $src/core/payments/routing/transformers.rs-e $src/types/transformers.rs-e cd $conn/ # Generate template files for the connector From 0fa8ad1b7c27010bf83e4035de9881d29e192e8a Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:58:36 +0530 Subject: [PATCH 078/443] docs(try_local_system): add instructions to run using Docker Compose by pulling standalone images (#2984) --- README.md | 44 +++-- docker-compose-development.yml | 301 +++++++++++++++++++++++++++++++++ docker-compose.yml | 238 +++++++++++++------------- docs/try_local_system.md | 128 +++++++++++--- 4 files changed, 542 insertions(+), 169 deletions(-) create mode 100644 docker-compose-development.yml diff --git a/README.md b/README.md index e820b93e63cc..db8e820ef142 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Hyperswitch-Logo

-

The open-source payments switch

@@ -35,7 +34,6 @@ The single API to access payment ecosystems across 130+ countries

-
@@ -57,14 +55,14 @@ Using Hyperswitch, you can:

⚡️ Quick Start Guide

-

One-click deployment on AWS cloud

+### One-click deployment on AWS cloud -The fastest and easiest way to try hyperswitch is via our CDK scripts +The fastest and easiest way to try Hyperswitch is via our CDK scripts 1. Click on the following button for a quick standalone deployment on AWS, suitable for prototyping. No code or setup is required in your system and the deployment is covered within the AWS free-tier setup. -   + 2. Sign-in to your AWS console. @@ -72,12 +70,27 @@ The fastest and easiest way to try hyperswitch is via our CDK scripts For an early access to the production-ready setup fill this Early Access Form +### Run it on your system + +You can run Hyperswitch on your system using Docker Compose after cloning this repository: + +```shell +docker compose up -d +``` + +This will start the payments router, the primary component within Hyperswitch. + +Check out the [local setup guide][local-setup-guide] for a more comprehensive +setup, which includes the [scheduler and monitoring services][docker-compose-scheduler-monitoring]. + +[local-setup-guide]: /docs/try_local_system.md +[docker-compose-scheduler-monitoring]: /docs/try_local_system.md#run-the-scheduler-and-monitoring-services +

🔌 Fast Integration for Stripe Users

-If you are already using Stripe, integrating with Hyperswitch is fun, fast & -easy. +If you are already using Stripe, integrating with Hyperswitch is fun, fast & easy. Try the steps below to get a feel for how quick the setup is: 1. Get API keys from our [dashboard]. @@ -96,9 +109,7 @@ Try the steps below to get a feel for how quick the setup is: As of Sept 2023, we support 50+ payment processors and multiple global payment methods. In addition, we are continuously integrating new processors based on their reach and community requests. Our target is to support 100+ processors by H2 2023. -You can find the latest list of payment processors, supported methods, and -features -[here][supported-connectors-and-features]. +You can find the latest list of payment processors, supported methods, and features [here][supported-connectors-and-features]. [supported-connectors-and-features]: https://hyperswitch.io/pm-list @@ -252,12 +263,11 @@ We welcome contributions from the community. Please read through our Included are directions for opening issues, coding standards, and notes on development. -- We appreciate all types of contributions: code, documentation, demo creation, or something new way you want to contribute to us. We will reward every contribution with a Hyperswitch branded t-shirt. -- 🦀 **Important note for Rust developers**: We aim for contributions from the community -across a broad range of tracks. Hence, we have prioritised simplicity and code -readability over purely idiomatic code. For example, some of the code in core -functions (e.g., `payments_core`) is written to be more readable than -pure-idiomatic. +- We appreciate all types of contributions: code, documentation, demo creation, or some new way you want to contribute to us. + We will reward every contribution with a Hyperswitch branded t-shirt. +- 🦀 **Important note for Rust developers**: We aim for contributions from the community across a broad range of tracks. + Hence, we have prioritised simplicity and code readability over purely idiomatic code. + For example, some of the code in core functions (e.g., `payments_core`) is written to be more readable than pure-idiomatic.

👥 Community

@@ -269,7 +279,6 @@ Get updates on Hyperswitch development and chat with the community: - [Slack workspace][slack] for questions related to integrating hyperswitch, integrating a connector in hyperswitch, etc. - [GitHub Discussions][github-discussions] to drop feature requests or suggest anything payments-related you need for your stack. -[blog]: https://hyperswitch.io/blog [discord]: https://discord.gg/wJZ7DVW8mm [slack]: https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg [github-discussions]: https://github.com/juspay/hyperswitch/discussions @@ -314,7 +323,6 @@ Check the [CHANGELOG.md](./CHANGELOG.md) file for details. This product is licensed under the [Apache 2.0 License](LICENSE). -

✨ Thanks to all contributors

diff --git a/docker-compose-development.yml b/docker-compose-development.yml new file mode 100644 index 000000000000..500f397cfa30 --- /dev/null +++ b/docker-compose-development.yml @@ -0,0 +1,301 @@ +version: "3.8" + +volumes: + cargo_cache: + pg_data: + router_build_cache: + scheduler_build_cache: + drainer_build_cache: + redisinsight_store: + +networks: + router_net: + +services: + ### Dependencies + pg: + image: postgres:latest + ports: + - "5432:5432" + networks: + - router_net + volumes: + - pg_data:/VAR/LIB/POSTGRESQL/DATA + environment: + - POSTGRES_USER=db_user + - POSTGRES_PASSWORD=db_pass + - POSTGRES_DB=hyperswitch_db + + redis-standalone: + image: redis:7 + labels: + - redis + networks: + - router_net + ports: + - "6379" + + migration_runner: + image: rust:latest + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration --database-url postgres://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME} run'" + working_dir: /app + networks: + - router_net + volumes: + - ./:/app + environment: + - DATABASE_USER=db_user + - DATABASE_PASSWORD=db_pass + - DATABASE_HOST=pg + - DATABASE_PORT=5432 + - DATABASE_NAME=hyperswitch_db + + ### Application services + hyperswitch-server: + image: rust:latest + command: cargo run --bin router -- -f ./config/docker_compose.toml + working_dir: /app + ports: + - "8080:8080" + networks: + - router_net + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - router_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + labels: + logs: "promtail" + healthcheck: + test: curl --fail http://localhost:8080/health || exit 1 + interval: 120s + retries: 4 + start_period: 20s + timeout: 10s + + hyperswitch-producer: + image: rust:latest + command: cargo run --bin scheduler -- -f ./config/docker_compose.toml + working_dir: /app + networks: + - router_net + profiles: + - scheduler + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - scheduler_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + - SCHEDULER_FLOW=producer + depends_on: + hyperswitch-consumer: + condition: service_healthy + labels: + logs: "promtail" + + hyperswitch-consumer: + image: rust:latest + command: cargo run --bin scheduler -- -f ./config/docker_compose.toml + working_dir: /app + networks: + - router_net + profiles: + - scheduler + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - scheduler_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + - SCHEDULER_FLOW=consumer + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + healthcheck: + test: (ps -e | grep scheduler) || exit 1 + interval: 120s + retries: 4 + start_period: 30s + timeout: 10s + + hyperswitch-drainer: + image: rust:latest + command: cargo run --bin drainer -- -f ./config/docker_compose.toml + working_dir: /app + deploy: + replicas: ${DRAINER_INSTANCE_COUNT:-1} + networks: + - router_net + profiles: + - full_kv + volumes: + - ./:/app + - cargo_cache:/cargo_cache + - drainer_build_cache:/cargo_build_cache + environment: + - CARGO_HOME=/cargo_cache + - CARGO_TARGET_DIR=/cargo_build_cache + restart: unless-stopped + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + + ### Clustered Redis setup + redis-cluster: + image: redis:7 + deploy: + replicas: ${REDIS_CLUSTER_COUNT:-3} + command: redis-server /usr/local/etc/redis/redis.conf + profiles: + - clustered_redis + volumes: + - ./config/redis.conf:/usr/local/etc/redis/redis.conf + labels: + - redis + networks: + - router_net + ports: + - "6379" + - "16379" + + redis-init: + image: redis:7 + profiles: + - clustered_redis + depends_on: + - redis-cluster + networks: + - router_net + command: "bash -c 'export COUNT=${REDIS_CLUSTER_COUNT:-3} + + \ if [ $$COUNT -lt 3 ] + + \ then + + \ echo \"Minimum 3 nodes are needed for redis cluster\" + + \ exit 1 + + \ fi + + \ HOSTS=\"\" + + \ for ((c=1; c<=$$COUNT;c++)) + + \ do + + \ NODE=$COMPOSE_PROJECT_NAME-redis-cluster-$$c:6379 + + \ echo $$NODE + + \ HOSTS=\"$$HOSTS $$NODE\" + + \ done + + \ echo Creating a cluster with $$HOSTS + + \ redis-cli --cluster create $$HOSTS --cluster-yes + + \ '" + + ### Monitoring + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + networks: + - router_net + profiles: + - monitoring + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana.ini:/etc/grafana/grafana.ini + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + promtail: + image: grafana/promtail:latest + volumes: + - ./logs:/var/log/router + - ./config:/etc/promtail + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail.yaml + profiles: + - monitoring + networks: + - router_net + + loki: + image: grafana/loki:latest + ports: + - "3100" + command: -config.file=/etc/loki/loki.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config:/etc/loki + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: --config=/etc/otel-collector.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + ports: + - "4317" + - "8888" + - "8889" + + prometheus: + image: prom/prometheus:latest + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090" + restart: unless-stopped + + tempo: + image: grafana/tempo:latest + command: -config.file=/etc/tempo.yaml + volumes: + - ./config/tempo.yaml:/etc/tempo.yaml + networks: + - router_net + profiles: + - monitoring + ports: + - "3200" # tempo + - "4317" # otlp grpc + restart: unless-stopped + + redis-insight: + image: redislabs/redisinsight:latest + networks: + - router_net + profiles: + - full_kv + ports: + - "8001:8001" + volumes: + - redisinsight_store:/db diff --git a/docker-compose.yml b/docker-compose.yml index f4dce575132e..fd18906143f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,76 +1,16 @@ -version: "3.7" +version: "3.8" volumes: - cargo_cache: pg_data: - cargo_build_cache: - p_cargo_build_cache: - c_cargo_build_cache: redisinsight_store: - networks: router_net: - services: - promtail: - image: grafana/promtail:latest - volumes: - - ./logs:/var/log/router - - ./config:/etc/promtail - - /var/run/docker.sock:/var/run/docker.sock - command: -config.file=/etc/promtail/promtail.yaml - profiles: - - monitoring - networks: - - router_net - - loki: - image: grafana/loki:latest - ports: - - "3100" - command: -config.file=/etc/loki/loki.yaml - networks: - - router_net - profiles: - - monitoring - volumes: - - ./config:/etc/loki - - otel-collector: - image: otel/opentelemetry-collector-contrib:latest - command: --config=/etc/otel-collector.yaml - networks: - - router_net - profiles: - - monitoring - volumes: - - ./config/otel-collector.yaml:/etc/otel-collector.yaml - ports: - - "4317" - - "8888" - - "8889" - - grafana: - image: grafana/grafana:latest - ports: - - "3000:3000" - networks: - - router_net - profiles: - - monitoring - restart: unless-stopped - environment: - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_BASIC_ENABLED=false - volumes: - - ./config/grafana.ini:/etc/grafana/grafana.ini - - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml - + ### Dependencies pg: - image: postgres:14.5 + image: postgres:latest ports: - "5432:5432" networks: @@ -82,52 +22,59 @@ services: - POSTGRES_PASSWORD=db_pass - POSTGRES_DB=hyperswitch_db + redis-standalone: + image: redis:7 + labels: + - redis + networks: + - router_net + ports: + - "6379" + migration_runner: - image: rust:1.70 - command: "bash -c 'cargo install diesel_cli --no-default-features --features \"postgres\" && diesel migration --database-url postgres://db_user:db_pass@pg:5432/hyperswitch_db run'" + image: rust:latest + command: "bash -c 'cargo install diesel_cli --no-default-features --features postgres && diesel migration --database-url postgres://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME} run'" working_dir: /app networks: - router_net volumes: - ./:/app + environment: + - DATABASE_USER=db_user + - DATABASE_PASSWORD=db_pass + - DATABASE_HOST=pg + - DATABASE_PORT=5432 + - DATABASE_NAME=hyperswitch_db + ### Application services hyperswitch-server: - image: rust:1.70 - command: cargo run -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-router:standalone + command: /local/bin/router -f /local/config/docker_compose.toml ports: - "8080:8080" networks: - router_net volumes: - - ./:/app - - cargo_cache:/cargo_cache - - cargo_build_cache:/cargo_build_cache - environment: - - CARGO_TARGET_DIR=/cargo_build_cache + - ./config:/local/config labels: logs: "promtail" healthcheck: test: curl --fail http://localhost:8080/health || exit 1 - interval: 100s + interval: 10s retries: 3 - start_period: 20s + start_period: 5s timeout: 10s hyperswitch-producer: - image: rust:1.70 - command: cargo run --bin scheduler -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-producer:standalone + command: /local/bin/scheduler -f /local/config/docker_compose.toml networks: - router_net profiles: - scheduler volumes: - - ./:/app - - cargo_cache:/cargo_cache - - p_cargo_build_cache:/cargo_build_cache + - ./config:/local/config environment: - - CARGO_TARGET_DIR=/cargo_build_cache - SCHEDULER_FLOW=producer depends_on: hyperswitch-consumer: @@ -136,39 +83,54 @@ services: logs: "promtail" hyperswitch-consumer: - image: rust:1.70 - command: cargo run --bin scheduler -- -f ./config/docker_compose.toml - working_dir: /app + image: juspaydotin/hyperswitch-consumer:standalone + command: /local/bin/scheduler -f /local/config/docker_compose.toml networks: - router_net profiles: - scheduler volumes: - - ./:/app - - cargo_cache:/cargo_cache - - c_cargo_build_cache:/cargo_build_cache + - ./config:/local/config environment: - - CARGO_TARGET_DIR=/cargo_build_cache - SCHEDULER_FLOW=consumer depends_on: hyperswitch-server: condition: service_started - labels: logs: "promtail" - healthcheck: test: (ps -e | grep scheduler) || exit 1 - interval: 120s + interval: 10s retries: 3 - start_period: 30s + start_period: 5s timeout: 10s + hyperswitch-drainer: + image: juspaydotin/hyperswitch-drainer:standalone + command: /local/bin/drainer -f /local/config/docker_compose.toml + deploy: + replicas: ${DRAINER_INSTANCE_COUNT:-1} + networks: + - router_net + profiles: + - full_kv + volumes: + - ./config:/local/config + restart: unless-stopped + depends_on: + hyperswitch-server: + condition: service_started + labels: + logs: "promtail" + + ### Clustered Redis setup redis-cluster: image: redis:7 deploy: replicas: ${REDIS_CLUSTER_COUNT:-3} command: redis-server /usr/local/etc/redis/redis.conf + profiles: + - clustered_redis volumes: - ./config/redis.conf:/usr/local/etc/redis/redis.conf labels: @@ -179,17 +141,10 @@ services: - "6379" - "16379" - redis-standalone: - image: redis:7 - labels: - - redis - networks: - - router_net - ports: - - "6379" - redis-init: image: redis:7 + profiles: + - clustered_redis depends_on: - redis-cluster networks: @@ -226,16 +181,62 @@ services: \ '" - redis-insight: - image: redislabs/redisinsight:latest + ### Monitoring + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" networks: - router_net profiles: - - full_kv + - monitoring + restart: unless-stopped + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./config/grafana.ini:/etc/grafana/grafana.ini + - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml + + promtail: + image: grafana/promtail:latest + volumes: + - ./logs:/var/log/router + - ./config:/etc/promtail + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail.yaml + profiles: + - monitoring + networks: + - router_net + + loki: + image: grafana/loki:latest ports: - - "8001:8001" + - "3100" + command: -config.file=/etc/loki/loki.yaml + networks: + - router_net + profiles: + - monitoring volumes: - - redisinsight_store:/db + - ./config:/etc/loki + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: --config=/etc/otel-collector.yaml + networks: + - router_net + profiles: + - monitoring + volumes: + - ./config/otel-collector.yaml:/etc/otel-collector.yaml + ports: + - "4317" + - "8888" + - "8889" + prometheus: image: prom/prometheus:latest networks: @@ -261,25 +262,14 @@ services: - "3200" # tempo - "4317" # otlp grpc restart: unless-stopped - hyperswitch-drainer: - image: rust:1.70 - command: cargo run --bin drainer -- -f ./config/docker_compose.toml - working_dir: /app - deploy: - replicas: ${DRAINER_INSTANCE_COUNT:-1} + + redis-insight: + image: redislabs/redisinsight:latest networks: - router_net profiles: - full_kv + ports: + - "8001:8001" volumes: - - ./:/app - - cargo_cache:/cargo_cache - - cargo_build_cache:/cargo_build_cache - environment: - - CARGO_TARGET_DIR=/cargo_build_cache - restart: unless-stopped - depends_on: - hyperswitch-server: - condition: service_started - labels: - logs: "promtail" + - redisinsight_store:/db diff --git a/docs/try_local_system.md b/docs/try_local_system.md index 59df43f24810..a9cd080f26d5 100644 --- a/docs/try_local_system.md +++ b/docs/try_local_system.md @@ -1,23 +1,20 @@ # Try out hyperswitch on your system -**NOTE:** -This guide is aimed at users and developers who wish to set up hyperswitch on -their local systems and requires quite some time and effort. -If you'd prefer trying out hyperswitch quickly without the hassle of setting up -all dependencies, you can [try out hyperswitch sandbox environment][try-sandbox]. - -There are two options to set up hyperswitch on your system: - -1. Use Docker Compose -2. Set up a Rust environment and other dependencies on your system +The simplest way to run hyperswitch locally is +[with Docker Compose](#run-hyperswitch-using-docker-compose) by pulling the +latest images from Docker Hub. +However, if you're willing to modify the code and run it, or are a developer +contributing to hyperswitch, then you can either +[set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose), +or [set up a Rust environment on your system](#set-up-a-rust-environment-and-other-dependencies). Check the Table Of Contents to jump to the relevant section. -[try-sandbox]: ./try_sandbox.md - **Table Of Contents:** -- [Set up hyperswitch using Docker Compose](#set-up-hyperswitch-using-docker-compose) +- [Run hyperswitch using Docker Compose](#run-hyperswitch-using-docker-compose) + - [Run the scheduler and monitoring services](#run-the-scheduler-and-monitoring-services) +- [Set up a development environment using Docker Compose](#set-up-a-development-environment-using-docker-compose) - [Set up a Rust environment and other dependencies](#set-up-a-rust-environment-and-other-dependencies) - [Set up dependencies on Ubuntu-based systems](#set-up-dependencies-on-ubuntu-based-systems) - [Set up dependencies on Windows (Ubuntu on WSL2)](#set-up-dependencies-on-windows-ubuntu-on-wsl2) @@ -33,7 +30,7 @@ Check the Table Of Contents to jump to the relevant section. - [Create a Payment](#create-a-payment) - [Create a Refund](#create-a-refund) -## Set up hyperswitch using Docker Compose +## Run hyperswitch using Docker Compose 1. Install [Docker Compose][docker-compose-install]. 2. Clone the repository and switch to the project directory: @@ -54,15 +51,15 @@ Check the Table Of Contents to jump to the relevant section. docker compose up -d ``` -5. Run database migrations: - - ```shell - docker compose run hyperswitch-server bash -c \ - "cargo install diesel_cli && \ - diesel migration --database-url postgres://db_user:db_pass@pg:5432/hyperswitch_db run" - ``` + This should run the hyperswitch payments router, the primary component within + hyperswitch. + Wait for the `migration_runner` container to finish installing `diesel_cli` + and running migrations (approximately 2 minutes) before proceeding further. + You can also choose to + [run the scheduler and monitoring services](#run-the-scheduler-and-monitoring-services) + in addition to the payments router. -6. Verify that the server is up and running by hitting the health endpoint: +5. Verify that the server is up and running by hitting the health endpoint: ```shell curl --head --request GET 'http://localhost:8080/health' @@ -71,9 +68,86 @@ Check the Table Of Contents to jump to the relevant section. If the command returned a `200 OK` status code, proceed with [trying out our APIs](#try-out-our-apis). +### Run the scheduler and monitoring services + +You can run the scheduler and monitoring services by specifying suitable profile +names to the above Docker Compose command. +To understand more about the hyperswitch architecture and the components +involved, check out the [architecture document][architecture]. + +- To run the scheduler components (consumer and producer), you can specify + `--profile scheduler`: + + ```shell + docker compose --profile scheduler up -d + ``` + +- To run the monitoring services (Grafana, Promtail, Loki, Prometheus and Tempo), + you can specify `--profile monitoring`: + + ```shell + docker compose --profile monitoring up -d + ``` + + You can then access Grafana at `http://localhost:3000` and view application + logs using the "Explore" tab, select Loki as the data source, and select the + container to query logs from. + +- You can also specify multiple profile names by specifying the `--profile` flag + multiple times. + To run both the scheduler components and monitoring services, the Docker + Compose command would be: + + ```shell + docker compose --profile scheduler --profile monitoring up -d + ``` + +Once the services have been confirmed to be up and running, you can proceed with +[trying out our APIs](#try-out-our-apis) + [docker-compose-install]: https://docs.docker.com/compose/install/ [docker-compose-config]: /config/docker_compose.toml [docker-compose-yml]: /docker-compose.yml +[architecture]: /docs/architecture.md + +## Set up a development environment using Docker Compose + +1. Install [Docker Compose][docker-compose-install]. +2. Clone the repository and switch to the project directory: + + ```shell + git clone https://github.com/juspay/hyperswitch + cd hyperswitch + ``` + +3. (Optional) Configure the application using the + [`config/docker_compose.toml`][docker-compose-config] file. + The provided configuration should work as is. + If you do update the `docker_compose.toml` file, ensure to also update the + corresponding values in the [`docker-compose.yml`][docker-compose-yml] file. +4. Start all the services using Docker Compose: + + ```shell + docker compose --file docker-compose-development.yml up -d + ``` + + This will compile the payments router, the primary component within + hyperswitch and then start it. + Depending on the specifications of your machine, compilation can take + around 15 minutes. + +5. (Optional) You can also choose to + [start the scheduler and/or monitoring services](#run-the-scheduler-and-monitoring-services) + in addition to the payments router. + +6. Verify that the server is up and running by hitting the health endpoint: + + ```shell + curl --head --request GET 'http://localhost:8080/health' + ``` + + If the command returned a `200 OK` status code, proceed with + [trying out our APIs](#try-out-our-apis). ## Set up a Rust environment and other dependencies @@ -134,7 +208,7 @@ for your distribution and follow along. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 5. Make sure your system has the `pkg-config` package and OpenSSL installed: @@ -224,7 +298,7 @@ packages for your distribution and follow along. 6. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 7. Make sure your system has the `pkg-config` package and OpenSSL installed: @@ -260,7 +334,7 @@ You can opt to use your favorite package manager instead. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` 5. Install OpenSSL with `winget`: @@ -322,7 +396,7 @@ You can opt to use your favorite package manager instead. 4. Install `diesel_cli` using `cargo`: ```shell - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` If linking `diesel_cli` fails due to missing `libpq` (if the error message is @@ -333,7 +407,7 @@ You can opt to use your favorite package manager instead. brew install libpq export PQ_LIB_DIR="$(brew --prefix libpq)/lib" - cargo install diesel_cli --no-default-features --features "postgres" + cargo install diesel_cli --no-default-features --features postgres ``` You may also choose to persist the value of `PQ_LIB_DIR` in your shell From aee59e088a8e7c1b81aca1015c90c7b4fd07511d Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:36:58 +0000 Subject: [PATCH 079/443] test(postman): update postman collection files --- postman/collection-json/paypal.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index d9deae47f9af..a6ee545a9497 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -808,7 +808,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"paypal\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -990,7 +990,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"surcharge_details\":{\"surcharge_amount\":5,\"tax_amount\":5},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4012000033330026\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", From 34f7e83fe969b630ebe4142605d102340177f7d6 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde Date: Mon, 27 Nov 2023 20:21:45 +0530 Subject: [PATCH 080/443] chore(version): v1.90.0 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6197598e564..8b3abf1d5781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.90.0 (2023-11-27) + +### Features + +- **auth:** Add Authorization for JWT Authentication types ([#2973](https://github.com/juspay/hyperswitch/pull/2973)) ([`03c0a77`](https://github.com/juspay/hyperswitch/commit/03c0a772a99000acf4676db8ca2ce916036281d1)) +- **user:** Implement change password for user ([#2959](https://github.com/juspay/hyperswitch/pull/2959)) ([`bfa1645`](https://github.com/juspay/hyperswitch/commit/bfa1645b847fb881eb2370d5dbfef6fd0b53725d)) + +### Bug Fixes + +- **router:** Added validation to check total orderDetails amount equal to amount in request ([#2965](https://github.com/juspay/hyperswitch/pull/2965)) ([`37532d4`](https://github.com/juspay/hyperswitch/commit/37532d46f599a99e0e021b0455a6f02381005dd7)) +- Add prefix to connector_transaction_id ([#2981](https://github.com/juspay/hyperswitch/pull/2981)) ([`107c3b9`](https://github.com/juspay/hyperswitch/commit/107c3b99417dd7bca7b62741ad601485700f37be)) + +### Refactors + +- **connector:** [Nuvei] update error message ([#2867](https://github.com/juspay/hyperswitch/pull/2867)) ([`04b7c03`](https://github.com/juspay/hyperswitch/commit/04b7c0384dc9290bd60f49033fd35732527720f1)) + +### Testing + +- **postman:** Update postman collection files ([`aee59e0`](https://github.com/juspay/hyperswitch/commit/aee59e088a8e7c1b81aca1015c90c7b4fd07511d)) + +### Documentation + +- **try_local_system:** Add instructions to run using Docker Compose by pulling standalone images ([#2984](https://github.com/juspay/hyperswitch/pull/2984)) ([`0fa8ad1`](https://github.com/juspay/hyperswitch/commit/0fa8ad1b7c27010bf83e4035de9881d29e192e8a)) + +### Miscellaneous Tasks + +- **connector:** Update connector addition script ([#2801](https://github.com/juspay/hyperswitch/pull/2801)) ([`34953a0`](https://github.com/juspay/hyperswitch/commit/34953a046429fe0341e8469bd9b036e176bda205)) + +**Full Changelog:** [`v1.89.0...v1.90.0`](https://github.com/juspay/hyperswitch/compare/v1.89.0...v1.90.0) + +- - - + + ## 1.89.0 (2023-11-24) ### Features From 54d6b1083fab5d2b0c7637c150524460a16a3fec Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:34:49 +0530 Subject: [PATCH 081/443] CI: update the credentails for generating token in release new version workflow (#2989) --- .github/workflows/release-new-version.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml index b54e240d96fc..2f8ae7e4819f 100644 --- a/.github/workflows/release-new-version.yml +++ b/.github/workflows/release-new-version.yml @@ -23,19 +23,11 @@ jobs: runs-on: ubuntu-latest steps: - - name: Generate a token - if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} - id: generate_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} - private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} - - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ steps.generate_token.outputs.token }} + token: ${{ secrets.AUTO_RELEASE_PAT }} - name: Install Rust uses: dtolnay/rust-toolchain@master @@ -51,8 +43,8 @@ jobs: - name: Set Git Configuration shell: bash run: | - git config --local user.name 'hyperswitch-bot[bot]' - git config --local user.email '148525504+hyperswitch-bot[bot]@users.noreply.github.com' + git config --local user.name 'github-actions' + git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' - name: Update Postman collection files from Postman directories shell: bash From 0e66b1b5dcce6dd87c9d743c9eb73d0cd8e330b2 Mon Sep 17 00:00:00 2001 From: nain-F49FF806 <126972030+nain-F49FF806@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:53:53 +0100 Subject: [PATCH 082/443] refactor(masking): use empty enums as masking:Strategy types (#2874) --- crates/cards/src/validate.rs | 2 +- crates/common_utils/src/pii.rs | 12 ++++++------ crates/masking/src/secret.rs | 6 +++--- crates/masking/src/strategy.rs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index db6957057ecc..d083a420a1e5 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -72,7 +72,7 @@ impl<'de> Deserialize<'de> for CardNumber { } } -pub struct CardNumberStrategy; +pub enum CardNumberStrategy {} impl Strategy for CardNumberStrategy where diff --git a/crates/common_utils/src/pii.rs b/crates/common_utils/src/pii.rs index c246d2042269..39793de5c2b5 100644 --- a/crates/common_utils/src/pii.rs +++ b/crates/common_utils/src/pii.rs @@ -27,7 +27,7 @@ pub type SecretSerdeValue = Secret; /// Strategy for masking a PhoneNumber #[derive(Debug)] -pub struct PhoneNumberStrategy; +pub enum PhoneNumberStrategy {} /// Phone Number #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -144,7 +144,7 @@ where /// Strategy for Encryption #[derive(Debug)] -pub struct EncryptionStratergy; +pub enum EncryptionStratergy {} impl Strategy for EncryptionStratergy where @@ -157,7 +157,7 @@ where /// Client secret #[derive(Debug)] -pub struct ClientSecret; +pub enum ClientSecret {} impl Strategy for ClientSecret where @@ -189,7 +189,7 @@ where /// Strategy for masking Email #[derive(Debug)] -pub struct EmailStrategy; +pub enum EmailStrategy {} impl Strategy for EmailStrategy where @@ -305,7 +305,7 @@ impl FromStr for Email { /// IP address #[derive(Debug)] -pub struct IpAddress; +pub enum IpAddress {} impl Strategy for IpAddress where @@ -332,7 +332,7 @@ where /// Strategy for masking UPI VPA's #[derive(Debug)] -pub struct UpiVpaMaskingStrategy; +pub enum UpiVpaMaskingStrategy {} impl Strategy for UpiVpaMaskingStrategy where diff --git a/crates/masking/src/secret.rs b/crates/masking/src/secret.rs index 96411d4632bd..b2e9124688cb 100644 --- a/crates/masking/src/secret.rs +++ b/crates/masking/src/secret.rs @@ -12,8 +12,8 @@ use crate::{strategy::Strategy, PeekInterface}; /// To get access to value use method `expose()` of trait [`crate::ExposeInterface`]. /// /// ## Masking -/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a unit struct -/// and pass the unit struct as a second generic parameter to [`Secret`] while defining it. +/// Use the [`crate::strategy::Strategy`] trait to implement a masking strategy on a zero-variant +/// enum and pass this enum as a second generic parameter to [`Secret`] while defining it. /// [`Secret`] will take care of applying the masking strategy on the inner secret when being /// displayed. /// @@ -24,7 +24,7 @@ use crate::{strategy::Strategy, PeekInterface}; /// use masking::Secret; /// use std::fmt; /// -/// struct MyStrategy; +/// enum MyStrategy {} /// /// impl Strategy for MyStrategy /// where diff --git a/crates/masking/src/strategy.rs b/crates/masking/src/strategy.rs index f744ee1f4b52..8b4d9b0ec34d 100644 --- a/crates/masking/src/strategy.rs +++ b/crates/masking/src/strategy.rs @@ -7,7 +7,7 @@ pub trait Strategy { } /// Debug with type -pub struct WithType; +pub enum WithType {} impl Strategy for WithType { fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -18,7 +18,7 @@ impl Strategy for WithType { } /// Debug without type -pub struct WithoutType; +pub enum WithoutType {} impl Strategy for WithoutType { fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { From c0116db271f6afc1b93c04705209bfc346228c68 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Tue, 28 Nov 2023 16:05:04 +0530 Subject: [PATCH 083/443] feat(currency_conversion): add currency conversion feature (#2948) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 259 +++++++- config/config.example.toml | 10 + config/development.toml | 9 + config/docker_compose.toml | 9 + crates/api_models/Cargo.toml | 2 +- crates/api_models/src/currency.rs | 21 + crates/api_models/src/lib.rs | 1 + crates/currency_conversion/Cargo.toml | 16 + crates/currency_conversion/src/conversion.rs | 101 +++ crates/currency_conversion/src/error.rs | 8 + crates/currency_conversion/src/lib.rs | 3 + crates/currency_conversion/src/types.rs | 201 ++++++ crates/euclid_wasm/Cargo.toml | 1 + crates/euclid_wasm/src/lib.rs | 36 ++ crates/router/Cargo.toml | 4 +- crates/router/src/configs/settings.rs | 33 + crates/router/src/core.rs | 2 + crates/router/src/core/currency.rs | 51 ++ crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 4 + crates/router/src/routes/app.rs | 20 +- crates/router/src/routes/currency.rs | 58 ++ crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/utils.rs | 6 +- crates/router/src/utils/currency.rs | 641 +++++++++++++++++++ crates/router_env/src/logger/types.rs | 2 + loadtest/config/development.toml | 9 + 27 files changed, 1501 insertions(+), 10 deletions(-) create mode 100644 crates/api_models/src/currency.rs create mode 100644 crates/currency_conversion/Cargo.toml create mode 100644 crates/currency_conversion/src/conversion.rs create mode 100644 crates/currency_conversion/src/error.rs create mode 100644 crates/currency_conversion/src/lib.rs create mode 100644 crates/currency_conversion/src/types.rs create mode 100644 crates/router/src/core/currency.rs create mode 100644 crates/router/src/routes/currency.rs create mode 100644 crates/router/src/utils/currency.rs diff --git a/Cargo.lock b/Cargo.lock index bf0ee2d110c7..e4a317d74f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,7 +381,7 @@ dependencies = [ "router_derive", "serde", "serde_json", - "strum 0.24.1", + "strum 0.25.0", "time", "url", "utoipa", @@ -1186,6 +1186,18 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -1227,6 +1239,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf617fabf5cdbdc92f774bfe5062d870f228b80056d41180797abf48bed4056e" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f404657a7ea7b5249e36808dff544bc88a28f26e0ac40009f674b7a009d14be3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.38", + "syn_derive", +] + [[package]] name = "brotli" version = "3.4.0" @@ -1264,6 +1300,28 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecount" version = "0.6.4" @@ -1415,6 +1473,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "checked_int_cast" version = "1.0.0" @@ -1858,6 +1922,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "currency_conversion" +version = "0.1.0" +dependencies = [ + "common_enums", + "rust_decimal", + "rusty-money", + "serde", + "thiserror", +] + [[package]] name = "darling" version = "0.20.3" @@ -2264,6 +2339,7 @@ name = "euclid_wasm" version = "0.1.0" dependencies = [ "api_models", + "currency_conversion", "euclid", "getrandom 0.2.10", "kgraph_utils", @@ -2501,6 +2577,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -4266,6 +4348,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4342,6 +4433,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -4415,6 +4526,12 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4643,6 +4760,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.22" @@ -4705,6 +4831,34 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rkyv" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ron" version = "0.7.1" @@ -4755,6 +4909,7 @@ dependencies = [ "common_enums", "common_utils", "config", + "currency_conversion", "data_models", "derive_deref", "diesel", @@ -4793,6 +4948,7 @@ dependencies = [ "router_derive", "router_env", "roxmltree", + "rust_decimal", "rustc-hash", "scheduler", "serde", @@ -4805,7 +4961,7 @@ dependencies = [ "sha-1 0.9.8", "sqlx", "storage_impl", - "strum 0.24.1", + "strum 0.25.0", "tera", "test_utils", "thiserror", @@ -4917,6 +5073,32 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" +dependencies = [ + "arrayvec", + "borsh", + "bytes 1.5.0", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e43721f4ef7060ebc2c3ede757733209564ca8207f47674181bcd425dd76945" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -5056,6 +5238,16 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", +] + [[package]] name = "ryu" version = "1.0.15" @@ -5136,6 +5328,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.9.2" @@ -5448,6 +5646,12 @@ dependencies = [ "tokio 1.32.0", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "simple_asn1" version = "0.6.2" @@ -5777,6 +5981,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5822,6 +6038,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.8.0" @@ -6329,7 +6551,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.10", ] [[package]] @@ -6351,7 +6573,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.4.11", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.0.2", + "toml_datetime", + "winnow 0.5.19", ] [[package]] @@ -7114,6 +7347,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -7156,6 +7398,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.15.1" diff --git a/config/config.example.toml b/config/config.example.toml index 7815f2400d04..0b8730ca114a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -53,6 +53,16 @@ default_hash_ttl = 900 # Default TTL for hashes entries, in seconds use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6) stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options +# This section provides configs for currency conversion api +[forex_api] +call_delay = 21600 # Api calls are made after every 6 hrs +local_fetch_retry_count = 5 # Fetch from Local cache has retry count as 5 +local_fetch_retry_delay = 1000 # Retry delay for checking write condition +api_timeout = 20000 # Api timeouts once it crosses 2000 ms +api_key = "YOUR API KEY HERE" # Api key for making request to foreign exchange Api +fallback_api_key = "YOUR API KEY" # Api key for the fallback service +redis_lock_timeout = 26000 # Redis remains write locked for 26000 ms once the acquire_redis_lock is called + # Logging configuration. Logging can be either to file or console or both. # Logging configuration for file logging diff --git a/config/development.toml b/config/development.toml index c82607a704c3..3d64a8791a1c 100644 --- a/config/development.toml +++ b/config/development.toml @@ -52,6 +52,15 @@ host_rs = "" mock_locker = true basilisk_host = "" +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [jwekey] locker_key_identifier1 = "" locker_key_identifier2 = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 986240f0a36b..445e1e856846 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -28,6 +28,15 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [replica_database] username = "db_user" password = "db_pass" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce882e913282..73c2d673c972 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -25,7 +25,7 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } diff --git a/crates/api_models/src/currency.rs b/crates/api_models/src/currency.rs new file mode 100644 index 000000000000..c1d7e422d041 --- /dev/null +++ b/crates/api_models/src/currency.rs @@ -0,0 +1,21 @@ +use common_utils::events::ApiEventMetric; + +/// QueryParams to be send to convert the amount -> from_currency -> to_currency +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CurrencyConversionParams { + pub amount: i64, + pub to_currency: String, + pub from_currency: String, +} + +/// Response to be send for convert currency route +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CurrencyConversionResponse { + pub converted_amount: String, + pub currency: String, +} + +impl ApiEventMetric for CurrencyConversionResponse {} +impl ApiEventMetric for CurrencyConversionParams {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 1abeff7b6ddb..ab40a96582bb 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod api_keys; pub mod bank_accounts; pub mod cards_info; pub mod conditional_configs; +pub mod currency; pub mod customers; pub mod disputes; pub mod enums; diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml new file mode 100644 index 000000000000..7eb3af7d526d --- /dev/null +++ b/crates/currency_conversion/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "currency_conversion" +description = "Currency conversion for cost based routing" +version = "0.1.0" +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +# First party crates +common_enums = { version = "0.1.0", path = "../common_enums", package = "common_enums" } + +# Third party crates +rust_decimal = "1.29" +rusty-money = { version = "0.4.0", features = ["iso", "crypto"] } +serde = { version = "1.0.163", features = ["derive"] } +thiserror = "1.0.43" diff --git a/crates/currency_conversion/src/conversion.rs b/crates/currency_conversion/src/conversion.rs new file mode 100644 index 000000000000..4cdca8fe0ea2 --- /dev/null +++ b/crates/currency_conversion/src/conversion.rs @@ -0,0 +1,101 @@ +use common_enums::Currency; +use rust_decimal::Decimal; +use rusty_money::Money; + +use crate::{ + error::CurrencyConversionError, + types::{currency_match, ExchangeRates}, +}; + +pub fn convert( + ex_rates: &ExchangeRates, + from_currency: Currency, + to_currency: Currency, + amount: i64, +) -> Result { + let money_minor = Money::from_minor(amount, currency_match(from_currency)); + let base_currency = ex_rates.base_currency; + if to_currency == base_currency { + ex_rates.forward_conversion(*money_minor.amount(), from_currency) + } else if from_currency == base_currency { + ex_rates.backward_conversion(*money_minor.amount(), to_currency) + } else { + let base_conversion_amt = + ex_rates.forward_conversion(*money_minor.amount(), from_currency)?; + ex_rates.backward_conversion(base_conversion_amt, to_currency) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + use std::collections::HashMap; + + use crate::types::CurrencyFactors; + #[test] + fn currency_to_currency_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let szl_conversion_rates = + CurrencyFactors::new(Decimal::new(194423, 4), Decimal::new(514, 4)); + let convert_from = Currency::SZL; + let convert_to = Currency::INR; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, inr_conversion_rates); + conversion.insert(convert_to, szl_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } + + #[test] + fn currency_to_base_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + let convert_from = Currency::INR; + let convert_to = Currency::USD; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, inr_conversion_rates); + conversion.insert(convert_to, usd_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } + + #[test] + fn base_to_currency_conversion() { + use super::*; + let mut conversion: HashMap = HashMap::new(); + let inr_conversion_rates = + CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5)); + let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0)); + let convert_from = Currency::USD; + let convert_to = Currency::INR; + let amount = 2000; + let base_currency = Currency::USD; + conversion.insert(convert_from, usd_conversion_rates); + conversion.insert(convert_to, inr_conversion_rates); + let sample_rate = ExchangeRates::new(base_currency, conversion); + let res = + convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency"); + println!( + "The conversion from {} {} to {} is {:?}", + amount, convert_from, convert_to, res + ); + } +} diff --git a/crates/currency_conversion/src/error.rs b/crates/currency_conversion/src/error.rs new file mode 100644 index 000000000000..b04c147147c3 --- /dev/null +++ b/crates/currency_conversion/src/error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error, serde::Serialize)] +#[serde(tag = "type", content = "info", rename_all = "snake_case")] +pub enum CurrencyConversionError { + #[error("Currency Conversion isn't possible")] + DecimalMultiplicationFailed, + #[error("Currency not supported: '{0}'")] + ConversionNotSupported(String), +} diff --git a/crates/currency_conversion/src/lib.rs b/crates/currency_conversion/src/lib.rs new file mode 100644 index 000000000000..48e1ae11e5d3 --- /dev/null +++ b/crates/currency_conversion/src/lib.rs @@ -0,0 +1,3 @@ +pub mod conversion; +pub mod error; +pub mod types; diff --git a/crates/currency_conversion/src/types.rs b/crates/currency_conversion/src/types.rs new file mode 100644 index 000000000000..fec25b9fc601 --- /dev/null +++ b/crates/currency_conversion/src/types.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +use common_enums::Currency; +use rust_decimal::Decimal; +use rusty_money::iso; + +use crate::error::CurrencyConversionError; + +/// Cached currency store of base currency +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ExchangeRates { + pub base_currency: Currency, + pub conversion: HashMap, +} + +/// Stores the multiplicative factor for conversion between currency to base and vice versa +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CurrencyFactors { + /// The factor that will be multiplied to provide Currency output + pub to_factor: Decimal, + /// The factor that will be multiplied to provide for the base output + pub from_factor: Decimal, +} + +impl CurrencyFactors { + pub fn new(to_factor: Decimal, from_factor: Decimal) -> Self { + Self { + to_factor, + from_factor, + } + } +} + +impl ExchangeRates { + pub fn new(base_currency: Currency, conversion: HashMap) -> Self { + Self { + base_currency, + conversion, + } + } + + /// The flow here is from_currency -> base_currency -> to_currency + /// from to_currency -> base currency + pub fn forward_conversion( + &self, + amt: Decimal, + from_currency: Currency, + ) -> Result { + let from_factor = self + .conversion + .get(&from_currency) + .ok_or_else(|| { + CurrencyConversionError::ConversionNotSupported(from_currency.to_string()) + })? + .from_factor; + amt.checked_mul(from_factor) + .ok_or(CurrencyConversionError::DecimalMultiplicationFailed) + } + + /// from base_currency -> to_currency + pub fn backward_conversion( + &self, + amt: Decimal, + to_currency: Currency, + ) -> Result { + let to_factor = self + .conversion + .get(&to_currency) + .ok_or_else(|| { + CurrencyConversionError::ConversionNotSupported(to_currency.to_string()) + })? + .to_factor; + amt.checked_mul(to_factor) + .ok_or(CurrencyConversionError::DecimalMultiplicationFailed) + } +} + +pub fn currency_match(currency: Currency) -> &'static iso::Currency { + match currency { + Currency::AED => iso::AED, + Currency::ALL => iso::ALL, + Currency::AMD => iso::AMD, + Currency::ANG => iso::ANG, + Currency::ARS => iso::ARS, + Currency::AUD => iso::AUD, + Currency::AWG => iso::AWG, + Currency::AZN => iso::AZN, + Currency::BBD => iso::BBD, + Currency::BDT => iso::BDT, + Currency::BHD => iso::BHD, + Currency::BIF => iso::BIF, + Currency::BMD => iso::BMD, + Currency::BND => iso::BND, + Currency::BOB => iso::BOB, + Currency::BRL => iso::BRL, + Currency::BSD => iso::BSD, + Currency::BWP => iso::BWP, + Currency::BZD => iso::BZD, + Currency::CAD => iso::CAD, + Currency::CHF => iso::CHF, + Currency::CLP => iso::CLP, + Currency::CNY => iso::CNY, + Currency::COP => iso::COP, + Currency::CRC => iso::CRC, + Currency::CUP => iso::CUP, + Currency::CZK => iso::CZK, + Currency::DJF => iso::DJF, + Currency::DKK => iso::DKK, + Currency::DOP => iso::DOP, + Currency::DZD => iso::DZD, + Currency::EGP => iso::EGP, + Currency::ETB => iso::ETB, + Currency::EUR => iso::EUR, + Currency::FJD => iso::FJD, + Currency::GBP => iso::GBP, + Currency::GHS => iso::GHS, + Currency::GIP => iso::GIP, + Currency::GMD => iso::GMD, + Currency::GNF => iso::GNF, + Currency::GTQ => iso::GTQ, + Currency::GYD => iso::GYD, + Currency::HKD => iso::HKD, + Currency::HNL => iso::HNL, + Currency::HRK => iso::HRK, + Currency::HTG => iso::HTG, + Currency::HUF => iso::HUF, + Currency::IDR => iso::IDR, + Currency::ILS => iso::ILS, + Currency::INR => iso::INR, + Currency::JMD => iso::JMD, + Currency::JOD => iso::JOD, + Currency::JPY => iso::JPY, + Currency::KES => iso::KES, + Currency::KGS => iso::KGS, + Currency::KHR => iso::KHR, + Currency::KMF => iso::KMF, + Currency::KRW => iso::KRW, + Currency::KWD => iso::KWD, + Currency::KYD => iso::KYD, + Currency::KZT => iso::KZT, + Currency::LAK => iso::LAK, + Currency::LBP => iso::LBP, + Currency::LKR => iso::LKR, + Currency::LRD => iso::LRD, + Currency::LSL => iso::LSL, + Currency::MAD => iso::MAD, + Currency::MDL => iso::MDL, + Currency::MGA => iso::MGA, + Currency::MKD => iso::MKD, + Currency::MMK => iso::MMK, + Currency::MNT => iso::MNT, + Currency::MOP => iso::MOP, + Currency::MUR => iso::MUR, + Currency::MVR => iso::MVR, + Currency::MWK => iso::MWK, + Currency::MXN => iso::MXN, + Currency::MYR => iso::MYR, + Currency::NAD => iso::NAD, + Currency::NGN => iso::NGN, + Currency::NIO => iso::NIO, + Currency::NOK => iso::NOK, + Currency::NPR => iso::NPR, + Currency::NZD => iso::NZD, + Currency::OMR => iso::OMR, + Currency::PEN => iso::PEN, + Currency::PGK => iso::PGK, + Currency::PHP => iso::PHP, + Currency::PKR => iso::PKR, + Currency::PLN => iso::PLN, + Currency::PYG => iso::PYG, + Currency::QAR => iso::QAR, + Currency::RON => iso::RON, + Currency::RUB => iso::RUB, + Currency::RWF => iso::RWF, + Currency::SAR => iso::SAR, + Currency::SCR => iso::SCR, + Currency::SEK => iso::SEK, + Currency::SGD => iso::SGD, + Currency::SLL => iso::SLL, + Currency::SOS => iso::SOS, + Currency::SSP => iso::SSP, + Currency::SVC => iso::SVC, + Currency::SZL => iso::SZL, + Currency::THB => iso::THB, + Currency::TTD => iso::TTD, + Currency::TRY => iso::TRY, + Currency::TWD => iso::TWD, + Currency::TZS => iso::TZS, + Currency::UGX => iso::UGX, + Currency::USD => iso::USD, + Currency::UYU => iso::UYU, + Currency::UZS => iso::UZS, + Currency::VND => iso::VND, + Currency::VUV => iso::VUV, + Currency::XAF => iso::XAF, + Currency::XOF => iso::XOF, + Currency::XPF => iso::XPF, + Currency::YER => iso::YER, + Currency::ZAR => iso::ZAR, + } +} diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 4fc8cd970f40..47e349847ef7 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -17,6 +17,7 @@ dummy_connector = ["kgraph_utils/dummy_connector"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index e85a002544ff..48d9ac0d82a8 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,6 +7,9 @@ use std::{ }; use api_models::{admin as admin_api, routing::ConnectorSelection}; +use currency_conversion::{ + conversion::convert as convert_currency, types as currency_conversion_types, +}; use euclid::{ backend::{inputs, interpreter::InterpreterBackend, EuclidBackend}, dssa::{ @@ -33,6 +36,39 @@ struct SeedData<'a> { } static SEED_DATA: OnceCell> = OnceCell::new(); +static SEED_FOREX: OnceCell = OnceCell::new(); + +/// This function can be used by the frontend to educate wasm about the forex rates data. +/// The input argument is a struct fields base_currency and conversion where later is all the conversions associated with the base_currency +/// to all different currencies present. +#[wasm_bindgen(js_name = setForexData)] +pub fn seed_forex(forex: JsValue) -> JsResult { + let forex: currency_conversion_types::ExchangeRates = serde_wasm_bindgen::from_value(forex)?; + SEED_FOREX + .set(forex) + .map_err(|_| "Forex has already been seeded".to_string()) + .err_to_js()?; + + Ok(JsValue::NULL) +} + +/// This function can be used to perform currency_conversion on the input amount, from_currency, +/// to_currency which are all expected to be one of currencies we already have in our Currency +/// enum. +#[wasm_bindgen(js_name = convertCurrency)] +pub fn convert_forex_value(amount: i64, from_currency: JsValue, to_currency: JsValue) -> JsResult { + let forex_data = SEED_FOREX + .get() + .ok_or("Forex Data not seeded") + .err_to_js()?; + let from_currency: enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; + let to_currency: enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; + let converted_amount = convert_currency(forex_data, from_currency, to_currency, amount) + .map_err(|_| "conversion not possible for provided values") + .err_to_js()?; + + Ok(serde_wasm_bindgen::to_value(&converted_amount)?) +} /// This function can be used by the frontend to provide the WASM with information about /// all the merchant's connector accounts. The input argument is a vector of all the merchant's diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 25feb373b734..f0316d69249e 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -76,6 +76,7 @@ regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = "0.16.20" roxmltree = "0.18.0" +rust_decimal = { version = "1.30.0", features = ["serde-with-float", "serde-with-str"] } rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" @@ -85,7 +86,7 @@ serde_urlencoded = "0.7.1" serde_with = "3.0.0" sha-1 = { version = "0.9" } sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } tera = "1.19.1" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } @@ -104,6 +105,7 @@ api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] cards = { version = "0.1.0", path = "../cards" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } +currency_conversion = { version = "0.1.0", path = "../currency_conversion" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 0007e636926c..cc273f93ee9a 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -13,6 +13,7 @@ use external_services::email::EmailSettings; use external_services::kms; use redis_interface::RedisSettings; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; +use rust_decimal::Decimal; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; @@ -70,6 +71,7 @@ pub struct Settings { pub secrets: Secrets, pub locker: Locker, pub connectors: Connectors, + pub forex_api: ForexApi, pub refund: Refund, pub eph_key: EphemeralConfig, pub scheduler: Option, @@ -119,6 +121,37 @@ pub struct PaymentLink { pub sdk_url: String, } +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct ForexApi { + pub local_fetch_retry_count: u64, + pub api_key: masking::Secret, + pub fallback_api_key: masking::Secret, + /// in ms + pub call_delay: i64, + /// in ms + pub local_fetch_retry_delay: u64, + /// in ms + pub api_timeout: u64, + /// in ms + pub redis_lock_timeout: u64, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct DefaultExchangeRates { + pub base_currency: String, + pub conversion: HashMap, + pub timestamp: i64, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Conversion { + #[serde(with = "rust_decimal::serde::str")] + pub to_factor: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub from_factor: Decimal, +} + #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct ApplepayMerchantConfigs { diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index a429cab482b4..cff2dc8e58f1 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -5,6 +5,8 @@ pub mod cache; pub mod cards_info; pub mod conditional_config; pub mod configs; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; pub mod errors; diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs new file mode 100644 index 000000000000..1ea9454f00a0 --- /dev/null +++ b/crates/router/src/core/currency.rs @@ -0,0 +1,51 @@ +use common_utils::errors::CustomResult; +use error_stack::ResultExt; + +use crate::{ + core::errors::ApiErrorResponse, + services::ApplicationResponse, + utils::currency::{self, convert_currency, get_forex_rates}, + AppState, +}; + +pub async fn retrieve_forex( + state: AppState, +) -> CustomResult, ApiErrorResponse> { + Ok(ApplicationResponse::Json( + get_forex_rates( + &state, + state.conf.forex_api.call_delay, + state.conf.forex_api.local_fetch_retry_delay, + state.conf.forex_api.local_fetch_retry_count, + #[cfg(feature = "kms")] + &state.conf.kms, + ) + .await + .change_context(ApiErrorResponse::GenericNotFoundError { + message: "Unable to fetch forex rates".to_string(), + })?, + )) +} + +pub async fn convert_forex( + state: AppState, + amount: i64, + to_currency: String, + from_currency: String, +) -> CustomResult< + ApplicationResponse, + ApiErrorResponse, +> { + Ok(ApplicationResponse::Json( + Box::pin(convert_currency( + state.clone(), + amount, + to_currency, + from_currency, + #[cfg(feature = "kms")] + &state.conf.kms, + )) + .await + .change_context(ApiErrorResponse::InternalServerError)?, + )) +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 0bc8e492c40c..2b1f9c692d86 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -122,6 +122,7 @@ pub fn mk_app( .service(routes::Payments::server(state.clone())) .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) + .service(routes::Forex::server(state.clone())) .service(routes::Refunds::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::Mandates::server(state.clone())) diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 5166e326fb91..37cc1339e1a1 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -4,6 +4,8 @@ pub mod app; pub mod cache; pub mod cards_info; pub mod configs; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod currency; pub mod customers; pub mod disputes; #[cfg(feature = "dummy_connector")] @@ -32,6 +34,8 @@ pub mod webhooks; pub mod locker_migration; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub use self::app::Forex; #[cfg(feature = "payouts")] pub use self::app::Payouts; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 84848e030120..ae0e0f04f598 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -10,6 +10,8 @@ use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; +#[cfg(any(feature = "olap", feature = "oltp"))] +use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; #[cfg(feature = "payouts")] @@ -28,7 +30,7 @@ use super::{cache::*, health::*}; use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; -use crate::{ +pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, events::{event_logger::EventLogger, EventHandler}, @@ -302,6 +304,22 @@ impl Payments { } } +#[cfg(any(feature = "olap", feature = "oltp"))] +pub struct Forex; + +#[cfg(any(feature = "olap", feature = "oltp"))] +impl Forex { + pub fn server(state: AppState) -> Scope { + web::scope("/forex") + .app_data(web::Data::new(state.clone())) + .app_data(web::Data::new(state.clone())) + .service(web::resource("/rates").route(web::get().to(currency::retrieve_forex))) + .service( + web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)), + ) + } +} + #[cfg(feature = "olap")] pub struct Routing; diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs new file mode 100644 index 000000000000..1e1858517176 --- /dev/null +++ b/crates/router/src/routes/currency.rs @@ -0,0 +1,58 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::Flow; + +use crate::{ + core::{api_locking, currency}, + routes::AppState, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::RetrieveForexFlow; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, _auth: auth::AuthenticationData, _| currency::retrieve_forex(state), + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::ForexRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn convert_forex( + state: web::Data, + req: HttpRequest, + params: web::Query, +) -> HttpResponse { + let flow = Flow::RetrieveForexFlow; + let amount = ¶ms.amount; + let to_currency = ¶ms.to_currency; + let from_currency = ¶ms.from_currency; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, _, _| { + currency::convert_forex( + state, + *amount, + to_currency.to_string(), + from_currency.to_string(), + ) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::ForexRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 219948bdd4d2..5c2ad123749c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,6 +23,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Forex, RustLockerMigration, Gsm, User, @@ -51,6 +52,8 @@ impl From for ApiIdentifier { | Flow::DecisionManagerRetrieveConfig | Flow::DecisionManagerUpsertConfig => Self::Routing, + Flow::RetrieveForexFlow => Self::Forex, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 901e84997e67..c936ee858c17 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,11 +1,11 @@ +pub mod currency; pub mod custom_serde; pub mod db_utils; pub mod ext_traits; -#[cfg(feature = "olap")] -pub mod user; - #[cfg(feature = "kv_store")] pub mod storage_partitioning; +#[cfg(feature = "olap")] +pub mod user; use std::fmt::Debug; diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs new file mode 100644 index 000000000000..118d9df28e22 --- /dev/null +++ b/crates/router/src/utils/currency.rs @@ -0,0 +1,641 @@ +use std::{collections::HashMap, ops::Deref, str::FromStr, sync::Arc, time::Duration}; + +use api_models::enums; +use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt}; +use currency_conversion::types::{CurrencyFactors, ExchangeRates}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; +use masking::PeekInterface; +use once_cell::sync::Lazy; +use redis_interface::DelReply; +use rust_decimal::Decimal; +use strum::IntoEnumIterator; +use tokio::{sync::RwLock, time::sleep}; + +use crate::{ + logger, + routes::app::settings::{Conversion, DefaultExchangeRates}, + services, AppState, +}; +const REDIX_FOREX_CACHE_KEY: &str = "{forex_cache}_lock"; +const REDIX_FOREX_CACHE_DATA: &str = "{forex_cache}_data"; +const FOREX_API_TIMEOUT: u64 = 5; +const FOREX_BASE_URL: &str = "https://openexchangerates.org/api/latest.json?app_id="; +const FOREX_BASE_CURRENCY: &str = "&base=USD"; +const FALLBACK_FOREX_BASE_URL: &str = "http://apilayer.net/api/live?access_key="; +const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD"; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FxExchangeRatesCacheEntry { + data: Arc, + timestamp: i64, +} + +static FX_EXCHANGE_RATES_CACHE: Lazy>> = + Lazy::new(|| RwLock::new(None)); + +impl ApiEventMetric for FxExchangeRatesCacheEntry {} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ForexCacheError { + #[error("API error")] + ApiError, + #[error("API timeout")] + ApiTimeout, + #[error("API unresponsive")] + ApiUnresponsive, + #[error("Conversion error")] + ConversionError, + #[error("Could not acquire the lock for cache entry")] + CouldNotAcquireLock, + #[error("Provided currency not acceptable")] + CurrencyNotAcceptable, + #[error("Incorrect entries in default Currency response")] + DefaultCurrencyParsingError, + #[error("Entry not found in cache")] + EntryNotFound, + #[error("Expiration time invalid")] + InvalidLogExpiry, + #[error("Error reading local")] + LocalReadError, + #[error("Error writing to local cache")] + LocalWriteError, + #[error("Json Parsing error")] + ParsingError, + #[error("Kms decryption error")] + KmsDecryptionFailed, + #[error("Error connecting to redis")] + RedisConnectionError, + #[error("Not able to release write lock")] + RedisLockReleaseFailed, + #[error("Error writing to redis")] + RedisWriteError, + #[error("Not able to acquire write lock")] + WriteLockNotAcquired, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct ForexResponse { + pub rates: HashMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct FallbackForexResponse { + pub quotes: HashMap, +} + +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +struct FloatDecimal(#[serde(with = "rust_decimal::serde::float")] Decimal); + +impl Deref for FloatDecimal { + type Target = Decimal; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FxExchangeRatesCacheEntry { + fn new(exchange_rate: ExchangeRates) -> Self { + Self { + data: Arc::new(exchange_rate), + timestamp: date_time::now_unix_timestamp(), + } + } + fn is_expired(&self, call_delay: i64) -> bool { + self.timestamp + call_delay < date_time::now_unix_timestamp() + } +} + +async fn retrieve_forex_from_local() -> Option { + FX_EXCHANGE_RATES_CACHE.read().await.clone() +} + +async fn save_forex_to_local( + exchange_rates_cache_entry: FxExchangeRatesCacheEntry, +) -> CustomResult<(), ForexCacheError> { + let mut local = FX_EXCHANGE_RATES_CACHE.write().await; + *local = Some(exchange_rates_cache_entry); + Ok(()) +} + +// Alternative handler for handling the case, When no data in local as well as redis +#[allow(dead_code)] +async fn waited_fetch_and_update_caches( + state: &AppState, + local_fetch_retry_delay: u64, + local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + for _n in 1..local_fetch_retry_count { + sleep(Duration::from_millis(local_fetch_retry_delay)).await; + //read from redis and update local plus break the loop and return + match retrieve_forex_from_redis(state).await { + Ok(Some(rates)) => { + save_forex_to_local(rates.clone()).await?; + return Ok(rates.clone()); + } + Ok(None) => continue, + Err(e) => { + logger::error!(?e); + continue; + } + } + } + //acquire lock one last time and try to fetch and update local & redis + successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await +} + +impl TryFrom for ExchangeRates { + type Error = error_stack::Report; + fn try_from(value: DefaultExchangeRates) -> Result { + let mut conversion_usable: HashMap = HashMap::new(); + for (curr, conversion) in value.conversion { + let enum_curr = enums::Currency::from_str(curr.as_str()) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + conversion_usable.insert(enum_curr, CurrencyFactors::from(conversion)); + } + let base_curr = enums::Currency::from_str(value.base_currency.as_str()) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + Ok(Self { + base_currency: base_curr, + conversion: conversion_usable, + }) + } +} + +impl From for CurrencyFactors { + fn from(value: Conversion) -> Self { + Self { + to_factor: value.to_factor, + from_factor: value.from_factor, + } + } +} +pub async fn get_forex_rates( + state: &AppState, + call_delay: i64, + local_fetch_retry_delay: u64, + local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + if let Some(local_rates) = retrieve_forex_from_local().await { + if local_rates.is_expired(call_delay) { + // expired local data + handler_local_expired( + state, + call_delay, + local_rates, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } else { + // Valid data present in local + Ok(local_rates) + } + } else { + // No data in local + handler_local_no_data( + state, + call_delay, + local_fetch_retry_delay, + local_fetch_retry_count, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } +} + +async fn handler_local_no_data( + state: &AppState, + call_delay: i64, + _local_fetch_retry_delay: u64, + _local_fetch_retry_count: u64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(Some(data)) => { + fallback_forex_redis_check( + state, + data, + call_delay, + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + Ok(None) => { + // No data in local as well as redis + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await?) + } + Err(err) => { + logger::error!(?err); + Ok(successive_fetch_and_save_forex( + state, + None, + #[cfg(feature = "kms")] + kms_config, + ) + .await?) + } + } +} + +async fn successive_fetch_and_save_forex( + state: &AppState, + stale_redis_data: Option, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match acquire_redis_lock(state).await { + Ok(lock_acquired) => { + if !lock_acquired { + return stale_redis_data.ok_or(ForexCacheError::CouldNotAcquireLock.into()); + } + let api_rates = fetch_forex_rates( + state, + #[cfg(feature = "kms")] + kms_config, + ) + .await; + match api_rates { + Ok(rates) => successive_save_data_to_redis_local(state, rates).await, + Err(err) => { + // API not able to fetch data call secondary service + logger::error!(?err); + let secondary_api_rates = fallback_fetch_forex_rates( + state, + #[cfg(feature = "kms")] + kms_config, + ) + .await; + match secondary_api_rates { + Ok(rates) => Ok(successive_save_data_to_redis_local(state, rates).await?), + Err(err) => stale_redis_data.ok_or({ + logger::error!(?err); + ForexCacheError::ApiUnresponsive.into() + }), + } + } + } + } + Err(e) => stale_redis_data.ok_or({ + logger::error!(?e); + ForexCacheError::ApiUnresponsive.into() + }), + } +} + +async fn successive_save_data_to_redis_local( + state: &AppState, + forex: FxExchangeRatesCacheEntry, +) -> CustomResult { + Ok(save_forex_to_redis(state, &forex) + .await + .async_and_then(|_rates| async { release_redis_lock(state).await }) + .await + .async_and_then(|_val| async { Ok(save_forex_to_local(forex.clone()).await) }) + .await + .map_or_else( + |e| { + logger::error!(?e); + forex.clone() + }, + |_| forex.clone(), + )) +} + +async fn fallback_forex_redis_check( + state: &AppState, + redis_data: FxExchangeRatesCacheEntry, + call_delay: i64, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await { + Some(redis_forex) => { + // Valid data present in redis + let exchange_rates = FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone()); + save_forex_to_local(exchange_rates.clone()).await?; + Ok(exchange_rates) + } + None => { + // redis expired + successive_fetch_and_save_forex( + state, + Some(redis_data), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } +} + +async fn handler_local_expired( + state: &AppState, + call_delay: i64, + local_rates: FxExchangeRatesCacheEntry, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + match retrieve_forex_from_redis(state).await { + Ok(redis_data) => { + match is_redis_expired(redis_data.as_ref(), call_delay).await { + Some(redis_forex) => { + // Valid data present in redis + let exchange_rates = + FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone()); + save_forex_to_local(exchange_rates.clone()).await?; + Ok(exchange_rates) + } + None => { + // Redis is expired going for API request + successive_fetch_and_save_forex( + state, + Some(local_rates), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } + } + Err(e) => { + // data not present in redis waited fetch + logger::error!(?e); + successive_fetch_and_save_forex( + state, + Some(local_rates), + #[cfg(feature = "kms")] + kms_config, + ) + .await + } + } +} + +async fn fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> Result> { + #[cfg(feature = "kms")] + let forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(state.conf.forex_api.api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let forex_api_key = state.conf.forex_api.api_key.peek(); + + let forex_url: String = format!("{}{}{}", FOREX_BASE_URL, forex_api_key, FOREX_BASE_CURRENCY); + let forex_request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&forex_url) + .build(); + + logger::info!(?forex_request); + let response = state + .api_client + .send_request( + &state.clone(), + forex_request, + Some(FOREX_API_TIMEOUT), + false, + ) + .await + .change_context(ForexCacheError::ApiUnresponsive)?; + let forex_response = response + .json::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", forex_response); + + let mut conversions: HashMap = HashMap::new(); + for enum_curr in enums::Currency::iter() { + match forex_response.rates.get(&enum_curr.to_string()) { + Some(rate) => { + let from_factor = match Decimal::new(1, 0).checked_div(**rate) { + Some(rate) => rate, + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + continue; + } + }; + let currency_factors = CurrencyFactors::new(**rate, from_factor); + conversions.insert(enum_curr, currency_factors); + } + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + } + }; + } + + Ok(FxExchangeRatesCacheEntry::new(ExchangeRates::new( + enums::Currency::USD, + conversions, + ))) +} + +pub async fn fallback_fetch_forex_rates( + state: &AppState, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + #[cfg(feature = "kms")] + let fallback_forex_api_key = kms::get_kms_client(kms_config) + .await + .decrypt(state.conf.forex_api.fallback_api_key.peek()) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "kms"))] + let fallback_forex_api_key = state.conf.forex_api.fallback_api_key.peek(); + + let fallback_forex_url: String = + format!("{}{}", FALLBACK_FOREX_BASE_URL, fallback_forex_api_key,); + let fallback_forex_request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&fallback_forex_url) + .build(); + + logger::info!(?fallback_forex_request); + let response = state + .api_client + .send_request( + &state.clone(), + fallback_forex_request, + Some(FOREX_API_TIMEOUT), + false, + ) + .await + .change_context(ForexCacheError::ApiUnresponsive)?; + let fallback_forex_response = response + .json::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", fallback_forex_response); + let mut conversions: HashMap = HashMap::new(); + for enum_curr in enums::Currency::iter() { + match fallback_forex_response.quotes.get( + format!( + "{}{}", + FALLBACK_FOREX_API_CURRENCY_PREFIX, + &enum_curr.to_string() + ) + .as_str(), + ) { + Some(rate) => { + let from_factor = match Decimal::new(1, 0).checked_div(**rate) { + Some(rate) => rate, + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + continue; + } + }; + let currency_factors = CurrencyFactors::new(**rate, from_factor); + conversions.insert(enum_curr, currency_factors); + } + None => { + logger::error!("Rates for {} not received from API", &enum_curr); + } + }; + } + + let rates = + FxExchangeRatesCacheEntry::new(ExchangeRates::new(enums::Currency::USD, conversions)); + match acquire_redis_lock(state).await { + Ok(_) => Ok(successive_save_data_to_redis_local(state, rates).await?), + Err(e) => { + logger::error!(?e); + Ok(rates) + } + } +} + +async fn release_redis_lock( + state: &AppState, +) -> Result> { + state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .delete_key(REDIX_FOREX_CACHE_KEY) + .await + .change_context(ForexCacheError::RedisLockReleaseFailed) +} + +async fn acquire_redis_lock(app_state: &AppState) -> CustomResult { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .set_key_if_not_exists_with_expiry( + REDIX_FOREX_CACHE_KEY, + "", + Some( + (app_state.conf.forex_api.local_fetch_retry_count + * app_state.conf.forex_api.local_fetch_retry_delay + + app_state.conf.forex_api.api_timeout) + .try_into() + .into_report() + .change_context(ForexCacheError::ConversionError)?, + ), + ) + .await + .map(|val| matches!(val, redis_interface::SetnxReply::KeySet)) + .change_context(ForexCacheError::CouldNotAcquireLock) +} + +async fn save_forex_to_redis( + app_state: &AppState, + forex_exchange_cache_entry: &FxExchangeRatesCacheEntry, +) -> CustomResult<(), ForexCacheError> { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .serialize_and_set_key(REDIX_FOREX_CACHE_DATA, forex_exchange_cache_entry) + .await + .change_context(ForexCacheError::RedisWriteError) +} + +async fn retrieve_forex_from_redis( + app_state: &AppState, +) -> CustomResult, ForexCacheError> { + app_state + .store + .get_redis_conn() + .change_context(ForexCacheError::RedisConnectionError)? + .get_and_deserialize_key(REDIX_FOREX_CACHE_DATA, "FxExchangeRatesCache") + .await + .change_context(ForexCacheError::EntryNotFound) +} + +async fn is_redis_expired( + redis_cache: Option<&FxExchangeRatesCacheEntry>, + call_delay: i64, +) -> Option> { + redis_cache.and_then(|cache| { + if cache.timestamp + call_delay > date_time::now_unix_timestamp() { + Some(cache.data.clone()) + } else { + None + } + }) +} + +pub async fn convert_currency( + state: AppState, + amount: i64, + to_currency: String, + from_currency: String, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + let rates = get_forex_rates( + &state, + state.conf.forex_api.call_delay, + state.conf.forex_api.local_fetch_retry_delay, + state.conf.forex_api.local_fetch_retry_count, + #[cfg(feature = "kms")] + kms_config, + ) + .await + .change_context(ForexCacheError::ApiError)?; + + let to_currency = api_models::enums::Currency::from_str(to_currency.as_str()) + .into_report() + .change_context(ForexCacheError::CurrencyNotAcceptable)?; + + let from_currency = api_models::enums::Currency::from_str(from_currency.as_str()) + .into_report() + .change_context(ForexCacheError::CurrencyNotAcceptable)?; + + let converted_amount = + currency_conversion::conversion::convert(&rates.data, from_currency, to_currency, amount) + .into_report() + .change_context(ForexCacheError::ConversionError)?; + + Ok(api_models::currency::CurrencyConversionResponse { + converted_amount: converted_amount.to_string(), + currency: to_currency.to_string(), + }) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7978e98e52c0..2a174f42eb63 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -163,6 +163,8 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + // Retrieve forex flow. + RetrieveForexFlow, /// Routing create flow, RoutingCreateConfig, /// Routing link config diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 2fb729fb7b90..bec1074b99d0 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -34,6 +34,15 @@ host_rs = "" mock_locker = true basilisk_host = "" +[forex_api] +call_delay = 21600 +local_fetch_retry_count = 5 +local_fetch_retry_delay = 1000 +api_timeout = 20000 +api_key = "YOUR API KEY HERE" +fallback_api_key = "YOUR API KEY HERE" +redis_lock_timeout = 26000 + [eph_key] validity = 1 From e7ad3a4db8823f3ae8d381771739670d8350e6da Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:22:33 +0530 Subject: [PATCH 084/443] feat(payment_methods): receive `card_holder_name` in confirm flow when using token for payment (#2982) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 15 +- .../router/src/connector/aci/transformers.rs | 3 +- .../src/connector/adyen/transformers.rs | 6 +- .../src/connector/airwallex/transformers.rs | 3 +- .../connector/bankofamerica/transformers.rs | 3 +- .../src/connector/bluesnap/transformers.rs | 297 +++++++++--------- .../braintree_graphql_transformers.rs | 21 +- .../src/connector/checkout/transformers.rs | 12 +- .../src/connector/cryptopay/transformers.rs | 3 +- .../src/connector/cybersource/transformers.rs | 3 +- .../src/connector/dlocal/transformers.rs | 3 +- .../src/connector/forte/transformers.rs | 3 +- .../src/connector/gocardless/transformers.rs | 23 +- .../src/connector/helcim/transformers.rs | 14 +- crates/router/src/connector/klarna.rs | 3 +- .../connector/multisafepay/transformers.rs | 6 +- .../src/connector/nexinets/transformers.rs | 3 +- .../router/src/connector/nmi/transformers.rs | 3 +- .../router/src/connector/noon/transformers.rs | 3 +- .../src/connector/nuvei/transformers.rs | 6 +- .../src/connector/opayo/transformers.rs | 3 +- .../src/connector/payeezy/transformers.rs | 3 +- .../src/connector/payme/transformers.rs | 10 +- .../src/connector/paypal/transformers.rs | 3 +- .../src/connector/powertranz/transformers.rs | 3 +- .../src/connector/shift4/transformers.rs | 12 +- .../src/connector/square/transformers.rs | 6 +- .../router/src/connector/stax/transformers.rs | 6 +- .../src/connector/stripe/transformers.rs | 16 +- .../src/connector/trustpay/transformers.rs | 3 +- .../router/src/connector/tsys/transformers.rs | 3 +- .../router/src/connector/volt/transformers.rs | 3 +- .../src/connector/worldline/transformers.rs | 3 +- .../src/connector/worldpay/transformers.rs | 3 +- .../router/src/connector/zen/transformers.rs | 3 +- crates/router/src/core/payment_methods.rs | 7 + crates/router/src/core/payments/helpers.rs | 64 +++- crates/router/src/openapi.rs | 1 + crates/router/src/types/transformers.rs | 3 +- openapi/openapi_spec.json | 24 ++ 40 files changed, 375 insertions(+), 237 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 74559f8ed69a..acb9bbdd6cd4 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -717,6 +717,14 @@ pub struct Card { pub nick_name: Option>, } +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct CardToken { + /// The card holder's name + #[schema(value_type = String, example = "John Test")] + pub card_holder_name: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum CardRedirectData { @@ -846,6 +854,7 @@ pub enum PaymentMethodData { Upi(UpiData), Voucher(VoucherData), GiftCard(Box), + CardToken(CardToken), } impl PaymentMethodData { @@ -873,7 +882,8 @@ impl PaymentMethodData { | Self::Reward | Self::Upi(_) | Self::Voucher(_) - | Self::GiftCard(_) => None, + | Self::GiftCard(_) + | Self::CardToken(_) => None, } } } @@ -1092,6 +1102,7 @@ pub enum AdditionalPaymentData { GiftCard {}, Voucher {}, CardRedirect {}, + CardToken {}, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] @@ -1660,6 +1671,7 @@ pub enum PaymentMethodDataResponse { Voucher, GiftCard, CardRedirect, + CardToken, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -2455,6 +2467,7 @@ impl From for PaymentMethodDataResponse { AdditionalPaymentData::Voucher {} => Self::Voucher, AdditionalPaymentData::GiftCard {} => Self::GiftCard, AdditionalPaymentData::CardRedirect {} => Self::CardRedirect, + AdditionalPaymentData::CardToken {} => Self::CardToken, } } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index f56369ed31ab..66aeb3bb6b2b 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -409,7 +409,8 @@ impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPayment | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.router_data.payment_method), connector: "Aci", })?, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index a75e3b8ff179..a130ac50cc04 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -1380,7 +1380,8 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> payments::PaymentMethodData::Crypto(_) | payments::PaymentMethodData::MandatePayment | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) => { + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Adyen", @@ -2276,7 +2277,8 @@ impl<'a> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: "Network tokenization for payment method".to_string(), connector: "Adyen", diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 457b8d075487..3785e02d4747 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -196,7 +196,8 @@ impl TryFrom<&AirwallexRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("airwallex"), )), }?; diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 70db9a6d8797..12170deb1a00 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -410,7 +410,8 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Bank of America"), ) diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index fe92c337a012..b4ed314e706a 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -221,7 +221,8 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::CardRedirect(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( "Selected payment method via Token flow through bluesnap".to_string(), )) @@ -240,160 +241,160 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, _ => BluesnapTxnType::AuthCapture, }; - let (payment_method, card_holder_info) = - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ref ccard) => Ok(( - PaymentMethodDetails::CreditCard(Card { - card_number: ccard.card_number.clone(), - expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.get_expiry_year_4_digit(), - security_code: ccard.card_cvc.clone(), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )), - api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - api_models::payments::WalletData::GooglePay(payment_method_data) => { - let gpay_object = - Encode::::encode_to_string_of_json( - &BluesnapGooglePayObject { - payment_method_data: utils::GooglePayWalletData::from( - payment_method_data, - ), - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::GooglePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(gpay_object), - ), - }), - None, - )) + let (payment_method, card_holder_info) = match item + .router_data + .request + .payment_method_data + .clone() + { + api::PaymentMethodData::Card(ref ccard) => Ok(( + PaymentMethodDetails::CreditCard(Card { + card_number: ccard.card_number.clone(), + expiration_month: ccard.card_exp_month.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), + security_code: ccard.card_cvc.clone(), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )), + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + api_models::payments::WalletData::GooglePay(payment_method_data) => { + let gpay_object = Encode::::encode_to_string_of_json( + &BluesnapGooglePayObject { + payment_method_data: utils::GooglePayWalletData::from( + payment_method_data, + ), + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::GooglePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(gpay_object), + ), + }), + None, + )) + } + api_models::payments::WalletData::ApplePay(payment_method_data) => { + let apple_pay_payment_data = payment_method_data + .get_applepay_decoded_payment_data() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let apple_pay_payment_data: ApplePayEncodedPaymentData = apple_pay_payment_data + .expose()[..] + .as_bytes() + .parse_struct("ApplePayEncodedPaymentData") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing = item + .router_data + .address + .billing + .to_owned() + .get_required_value("billing") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let billing_address = billing + .address + .get_required_value("billing_address") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let mut address = Vec::new(); + if let Some(add) = billing_address.line1.to_owned() { + address.push(add) } - api_models::payments::WalletData::ApplePay(payment_method_data) => { - let apple_pay_payment_data = payment_method_data - .get_applepay_decoded_payment_data() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let apple_pay_payment_data: ApplePayEncodedPaymentData = - apple_pay_payment_data.expose()[..] - .as_bytes() - .parse_struct("ApplePayEncodedPaymentData") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - let billing = item - .router_data - .address - .billing - .to_owned() - .get_required_value("billing") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let billing_address = billing - .address - .get_required_value("billing_address") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let mut address = Vec::new(); - if let Some(add) = billing_address.line1.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line2.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line3.to_owned() { - address.push(add) - } - - let apple_pay_object = - Encode::::encode_to_string_of_json( - &EncodedPaymentToken { - token: ApplepayPaymentData { - payment_data: apple_pay_payment_data, - payment_method: payment_method_data - .payment_method - .to_owned() - .into(), - transaction_identifier: payment_method_data - .transaction_identifier, - }, - billing_contact: BillingDetails { - country_code: billing_address.country, - address_lines: Some(address), - family_name: billing_address.last_name.to_owned(), - given_name: billing_address.first_name.to_owned(), - postal_code: billing_address.zip, - }, - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::ApplePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(apple_pay_object), - ), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )) + if let Some(add) = billing_address.line2.to_owned() { + address.push(add) } - payments::WalletData::AliPayQr(_) - | payments::WalletData::AliPayRedirect(_) - | payments::WalletData::AliPayHkRedirect(_) - | payments::WalletData::MomoRedirect(_) - | payments::WalletData::KakaoPayRedirect(_) - | payments::WalletData::GoPayRedirect(_) - | payments::WalletData::GcashRedirect(_) - | payments::WalletData::ApplePayRedirect(_) - | payments::WalletData::ApplePayThirdPartySdk(_) - | payments::WalletData::DanaRedirect {} - | payments::WalletData::GooglePayRedirect(_) - | payments::WalletData::GooglePayThirdPartySdk(_) - | payments::WalletData::MbWayRedirect(_) - | payments::WalletData::MobilePayRedirect(_) - | payments::WalletData::PaypalRedirect(_) - | payments::WalletData::PaypalSdk(_) - | payments::WalletData::SamsungPay(_) - | payments::WalletData::TwintRedirect {} - | payments::WalletData::VippsRedirect {} - | payments::WalletData::TouchNGoRedirect(_) - | payments::WalletData::WeChatPayRedirect(_) - | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) - | payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("bluesnap"), - )) + if let Some(add) = billing_address.line3.to_owned() { + address.push(add) } - }, - payments::PaymentMethodData::PayLater(_) - | payments::PaymentMethodData::BankRedirect(_) - | payments::PaymentMethodData::BankDebit(_) - | payments::PaymentMethodData::BankTransfer(_) - | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::MandatePayment - | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) - | payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + + let apple_pay_object = Encode::::encode_to_string_of_json( + &EncodedPaymentToken { + token: ApplepayPaymentData { + payment_data: apple_pay_payment_data, + payment_method: payment_method_data + .payment_method + .to_owned() + .into(), + transaction_identifier: payment_method_data.transaction_identifier, + }, + billing_contact: BillingDetails { + country_code: billing_address.country, + address_lines: Some(address), + family_name: billing_address.last_name.to_owned(), + given_name: billing_address.first_name.to_owned(), + postal_code: billing_address.zip, + }, + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::ApplePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(apple_pay_object), + ), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) + | payments::WalletData::WeChatPayQr(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("bluesnap"), )) } - }?; + }, + payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("bluesnap"), + )), + }?; Ok(Self { amount: item.amount.to_owned(), payment_method, diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index 5069a9fe38d2..009177e961e7 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("braintree"), ) @@ -879,12 +880,11 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("braintree"), + ) + .into()), } } } @@ -1423,9 +1423,10 @@ fn get_braintree_redirect_form( | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => Err( - errors::ConnectorError::NotImplemented("given payment method".to_owned()), - )?, + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "given payment method".to_owned(), + ))?, }, }) } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 90e65c8b0474..173ac0b8f585 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("checkout"), ) @@ -375,11 +376,10 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("checkout"), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + )), }?; let three_ds = match item.router_data.auth_type { diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 0bc4ff3b3ae6..446da0761d1f 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -80,7 +80,8 @@ impl TryFrom<&CryptopayRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "CryptoPay", diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 33b8fa56d00e..656c45b6d6b6 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -367,7 +367,8 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), ))? diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 668a335cce88..a9033e53d666 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -168,7 +168,8 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( crate::connector::utils::get_unimplemented_payment_method_error_message("Dlocal"), ))?, } diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index dd78324c9b8b..2197b4558a20 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -112,7 +112,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Forte", diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index 72204b511518..63e199657af0 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -108,7 +108,8 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for GocardlessCustomerRequest | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Gocardless"), )) @@ -297,12 +298,11 @@ impl TryFrom<&types::TokenizationRouterData> for CustomerBankAccount { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Gocardless"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Gocardless"), + ) + .into()), } } } @@ -483,11 +483,10 @@ impl TryFrom<&types::SetupMandateRouterData> for GocardlessMandateRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - "Setup Mandate flow for selected payment method through Gocardless".to_string(), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for selected payment method through Gocardless".to_string(), + )), }?; let payment_method_token = item.get_payment_method_token()?; let customer_bank_account = match payment_method_token { diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 9510ff6e67ad..9f405e2e2ea1 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -141,7 +141,8 @@ impl TryFrom<&types::SetupMandateRouterData> for HelcimVerifyRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Helcim", @@ -223,12 +224,11 @@ impl TryFrom<&HelcimRouterData<&types::PaymentsAuthorizeRouterData>> for HelcimP | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.router_data.request.payment_method_data), - connector: "Helcim", - })? - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.router_data.request.payment_method_data), + connector: "Helcim", + })?, } } } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 3bd3407c3aef..91eaf94c01ee 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -406,7 +406,8 @@ impl | api_payments::PaymentMethodData::Reward | api_payments::PaymentMethodData::Upi(_) | api_payments::PaymentMethodData::Voucher(_) - | api_payments::PaymentMethodData::GiftCard(_) => Err(error_stack::report!( + | api_payments::PaymentMethodData::GiftCard(_) + | api_payments::PaymentMethodData::CardToken(_) => Err(error_stack::report!( errors::ConnectorError::MismatchedPaymentData )), } diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index a067818b743d..1780b77379c7 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -365,7 +365,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; @@ -509,7 +510,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 2af3ee0a1bb8..15cbe9a7e28e 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -624,7 +624,8 @@ fn get_payment_details_and_product( | PaymentMethodData::Reward | PaymentMethodData::Upi(_) | PaymentMethodData::Voucher(_) - | PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nexinets"), ))?, } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index c8721d0d8f6b..ff3a1e6a1c54 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -188,7 +188,8 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "nmi", }) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 5ff92582051a..ee3a8ba8c532 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -284,7 +284,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { | api::PaymentMethodData::Reward {} | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: conn_utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Noon", diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index b79b2c892643..36244b8bc0d8 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -856,8 +856,9 @@ impl | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), ) @@ -1037,6 +1038,7 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPay | Some(api::PaymentMethodData::CardRedirect(..)) | Some(api::PaymentMethodData::Reward) | Some(api::PaymentMethodData::Upi(..)) + | Some(api::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), )), diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 41bcc1500ed1..5e9fb066c78d 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -52,7 +52,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Opayo"), ) .into()), diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 817ab43ac717..90c58c3a9bce 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -260,7 +260,8 @@ fn get_payment_method_data( | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Payeezy"), ))?, } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 092a8b49fd86..e751de20e219 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -431,7 +431,8 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod { | PaymentMethodData::GiftCard(_) | PaymentMethodData::CardRedirect(_) | PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => { + | PaymentMethodData::Voucher(_) + | PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) } } @@ -666,7 +667,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("payme"), ))?, } @@ -725,6 +727,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { | Some(api::PaymentMethodData::Upi(_)) | Some(api::PaymentMethodData::Voucher(_)) | Some(api::PaymentMethodData::GiftCard(_)) + | Some(api::PaymentMethodData::CardToken(_)) | None => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } @@ -761,7 +764,8 @@ impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index d023077ff008..e59ff09a1f60 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -584,7 +584,8 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Crypto(_) - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Paypal", diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 7f62c1939c07..a631a126ed3f 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -113,7 +113,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PowertranzPaymentsRequest | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "powertranz", }) diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 0dd3b8583490..c272a5b6fc12 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -166,11 +166,14 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotSupported { + message: utils::SELECTED_PAYMENT_METHOD.to_string(), + connector: "Shift4", + } + .into()) } - .into()), } } } @@ -397,6 +400,7 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { message: "Flow".to_string(), connector: "Shift4", diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index dfb49e8e6775..6024a20fa6ab 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -191,7 +191,8 @@ impl TryFrom<&types::TokenizationRouterData> for SquareTokenRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, @@ -307,7 +308,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index f2aae442ddd6..bb37bf1fc9e7 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -118,7 +118,8 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: "SELECTED_PAYMENT_METHOD".to_string(), connector: "Stax", })?, @@ -268,7 +269,8 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: "SELECTED_PAYMENT_METHOD".to_string(), connector: "Stax", })?, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 56eebc2df3bd..ae7fe59be96c 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1431,13 +1431,13 @@ fn create_stripe_payment_method( .into()), }, - payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::MandatePayment => { - Err(errors::ConnectorError::NotSupported { - message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), - connector: "stripe", - } - .into()) + payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), + connector: "stripe", } + .into()), } } @@ -2995,6 +2995,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for StripeCreditTransferSo | Some(payments::PaymentMethodData::GiftCard(..)) | Some(payments::PaymentMethodData::CardRedirect(..)) | Some(payments::PaymentMethodData::Voucher(..)) + | Some(payments::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), ) @@ -3416,7 +3417,8 @@ impl | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{pm_type:?}"), connector: "Stripe", })?, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 0210d3ca2d92..e891501d6d0a 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -445,7 +445,8 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsAuthorizeRouterData>> for Trust | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("trustpay"), ) .into()), diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index c60aeb64898b..863b754fc89c 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -77,7 +77,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("tsys"), ))?, } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index e603ef2db06c..efed7c797c76 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -148,7 +148,8 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Volt", diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 049453e325ae..282e1b3a8adb 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -257,7 +257,8 @@ impl | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldline"), ))?, }; diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index d31f4d65e78c..e35a51552c03 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -120,7 +120,8 @@ fn fetch_payment_instrument( | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldpay"), ) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 689894176b26..64f6d5bf1a07 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -707,7 +707,8 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment api_models::payments::PaymentMethodData::Crypto(_) | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ))? diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 80cec01e9166..1049137a9470 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -3,6 +3,7 @@ pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; +use api_models::payments::CardToken; pub use api_models::{ enums::{Connector, PayoutConnectors}, payouts as payout_types, @@ -42,6 +43,7 @@ pub trait PaymentMethodRetrieve { token: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult>; } @@ -125,6 +127,7 @@ impl PaymentMethodRetrieve for Oss { token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult> { match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { @@ -134,6 +137,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, card_cvc, merchant_key_store, + card_token_data, ) .await } @@ -145,6 +149,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, card_cvc, merchant_key_store, + card_token_data, ) .await } @@ -155,6 +160,7 @@ impl PaymentMethodRetrieve for Oss { &card_token.token, payment_intent, card_cvc, + card_token_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))) @@ -166,6 +172,7 @@ impl PaymentMethodRetrieve for Oss { &card_token.token, payment_intent, card_cvc, + card_token_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index f57c0640f1a8..68b8128d7909 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use api_models::payments::GetPaymentMethodType; +use api_models::payments::{CardToken, GetPaymentMethodType}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -1356,6 +1356,7 @@ pub async fn retrieve_payment_method_with_temporary_token( payment_intent: &PaymentIntent, card_cvc: Option>, merchant_key_store: &domain::MerchantKeyStore, + card_token_data: Option<&CardToken>, ) -> RouterResult> { let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) @@ -1375,9 +1376,29 @@ pub async fn retrieve_payment_method_with_temporary_token( Ok::<_, error_stack::Report>(match pm { Some(api::PaymentMethodData::Card(card)) => { + let mut updated_card = card.clone(); + let mut is_card_updated = false; + + let name_on_card = if card.card_holder_name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_holder_name", + })? + } else { + card.card_holder_name.clone() + }; + updated_card.card_holder_name = name_on_card; + if let Some(cvc) = card_cvc { - let mut updated_card = card; + is_card_updated = true; updated_card.card_cvc = cvc; + } + if is_card_updated { let updated_pm = api::PaymentMethodData::Card(updated_card); vault::Vault::store_payment_method_data_in_locker( state, @@ -1423,6 +1444,7 @@ pub async fn retrieve_card_with_permanent_token( token: &str, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult { let customer_id = payment_intent .customer_id @@ -1437,13 +1459,26 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker")?; + let name = card + .name_on_card + .get_required_value("name_on_card") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("card holder name was not saved in permanent locker")?; + + let name_on_card = if name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_holder_name", + })? + } else { + name + }; + let api_card = api::Card { card_number: card.card_number, - card_holder_name: card - .name_on_card - .get_required_value("name_on_card") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("card holder name was not saved in permanent locker")?, + card_holder_name: name_on_card, card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_cvc: card_cvc.unwrap_or_default(), @@ -1529,6 +1564,11 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let card_cvc = payment_data.card_cvc.clone(); + let card_token_data = request.as_ref().and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }); + // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { @@ -1538,6 +1578,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( &hyperswitch_token, &payment_data.payment_intent, card_cvc, + card_token_data, ) .await .attach_printable("in 'make_pm_data'")?; @@ -3316,6 +3357,9 @@ pub async fn get_additional_payment_data( api_models::payments::PaymentMethodData::GiftCard(_) => { api_models::payments::AdditionalPaymentData::GiftCard {} } + api_models::payments::PaymentMethodData::CardToken(_) => { + api_models::payments::AdditionalPaymentData::CardToken {} + } } } @@ -3615,6 +3659,12 @@ pub fn get_key_params_for_surcharge_details( gift_card.get_payment_method_type(), None, )), + api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } } } diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index d191890b8cdb..ec38389cdc42 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -248,6 +248,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::OnlineMandate, api_models::payments::Card, api_models::payments::CardRedirectData, + api_models::payments::CardToken, api_models::payments::CustomerAcceptance, api_models::payments::PaymentsRequest, api_models::payments::PaymentsCreateRequest, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 45aad93371e2..5bd28db3c158 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -522,7 +522,8 @@ impl ForeignTryFrom for api_enums::Paym payment_method_data: api_models::payments::PaymentMethodData, ) -> Result { match payment_method_data { - api_models::payments::PaymentMethodData::Card(..) => Ok(Self::Card), + api_models::payments::PaymentMethodData::Card(..) + | api_models::payments::PaymentMethodData::CardToken(..) => Ok(Self::Card), api_models::payments::PaymentMethodData::Wallet(..) => Ok(Self::Wallet), api_models::payments::PaymentMethodData::PayLater(..) => Ok(Self::PayLater), api_models::payments::PaymentMethodData::BankRedirect(..) => Ok(Self::BankRedirect), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 88a0d115ff01..08f415782963 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4053,6 +4053,19 @@ } ] }, + "CardToken": { + "type": "object", + "required": [ + "card_holder_name" + ], + "properties": { + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Test" + } + } + }, "CashappQr": { "type": "object" }, @@ -8657,6 +8670,17 @@ "$ref": "#/components/schemas/GiftCardData" } } + }, + { + "type": "object", + "required": [ + "card_token" + ], + "properties": { + "card_token": { + "$ref": "#/components/schemas/CardToken" + } + } } ] }, From 77fc92c99a99aaf76d270ba5b981928183a05768 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:40:42 +0530 Subject: [PATCH 085/443] feat(core): [Paypal] Add Preprocessing flow to CompleteAuthorize for Card 3DS Auth Verification (#2757) --- crates/router/src/connector/paypal.rs | 156 ++++++++++++++++++ .../src/connector/paypal/transformers.rs | 68 ++++++++ crates/router/src/consts.rs | 2 + crates/router/src/core/payments.rs | 16 +- crates/router/src/core/payments/flows.rs | 1 - .../src/core/payments/flows/authorize_flow.rs | 24 +++ .../payments/flows/complete_authorize_flow.rs | 72 +++++++- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/types.rs | 1 + 9 files changed, 338 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 4e50bc924b33..9ab19b295570 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -30,6 +30,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, + storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, ErrorResponse, Response, }, @@ -506,6 +507,161 @@ impl ConnectorIntegration for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let order_id = req + .request + .connector_transaction_id + .to_owned() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}v2/checkout/orders/{}?fields=payment_source", + self.base_url(connectors), + order_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalPreProcessingResponse = res + .response + .parse_struct("paypal PaypalPreProcessingResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + // permutation for status to continue payment + match ( + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .liability_shift + .clone(), + ) { + ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Success), + paypal::LiabilityShift::Possible, + ) + | ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Attempted), + paypal::LiabilityShift::Possible, + ) + | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + Ok(types::PaymentsPreProcessingRouterData { + status: storage_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..data.clone() + }) + } + _ => Ok(types::PaymentsPreProcessingRouterData { + response: Err(ErrorResponse { + attempt_status: Some(enums::AttemptStatus::Failure), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + connector_transaction_id: None, + reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", + consts::CANNOT_CONTINUE_AUTH, + response + .payment_source + .card + .authentication_result + .liability_shift, + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration< CompleteAuthorize, diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index e59ff09a1f60..04328cead233 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -925,6 +925,74 @@ pub struct PaypalThreeDsResponse { links: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalPreProcessingResponse { + pub payment_source: CardParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardParams { + pub card: AuthResult, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthResult { + pub authentication_result: PaypalThreeDsParams, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsParams { + pub liability_shift: LiabilityShift, + pub three_d_secure: ThreeDsCheck, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreeDsCheck { + pub enrollment_status: Option, + pub authentication_status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LiabilityShift { + Possible, + No, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EnrollementStatus { + Null, + #[serde(rename = "Y")] + Ready, + #[serde(rename = "N")] + NotReady, + #[serde(rename = "U")] + Unavailable, + #[serde(rename = "B")] + Bypassed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthenticationStatus { + Null, + #[serde(rename = "Y")] + Success, + #[serde(rename = "N")] + Failed, + #[serde(rename = "R")] + Rejected, + #[serde(rename = "A")] + Attempted, + #[serde(rename = "U")] + Unable, + #[serde(rename = "C")] + ChallengeRequired, + #[serde(rename = "I")] + InfoOnly, + #[serde(rename = "D")] + Decoupled, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaypalOrdersResponse { id: String, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index c5490ee00e63..8937764409f8 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -28,6 +28,8 @@ pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; +pub(crate) const CANNOT_CONTINUE_AUTH: &str = + "Cannot continue with Authorization due to failed Liability Shift."; // General purpose base64 engines pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose = diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1c40ef81f497..4fe7ea848008 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1418,7 +1418,21 @@ where (router_data, should_continue_payment) } } - _ => (router_data, should_continue_payment), + _ => { + // 3DS validation for paypal cards after verification (authorize call) + if connector.connector_name == router_types::Connector::Paypal + && payment_data.payment_attempt.payment_method + == Some(storage_enums::PaymentMethod::Card) + && matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + { + router_data = router_data.preprocessing_steps(state, connector).await?; + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } }; Ok(router_data_and_should_continue_payment) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 46eaca26f7cc..d983cd19bdb5 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -863,7 +863,6 @@ default_imp_for_pre_processing_steps!( connector::Opayo, connector::Opennode, connector::Payeezy, - connector::Paypal, connector::Payu, connector::Powertranz, connector::Prophetpay, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 04bd7f0b4338..4ef23f481a2c 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -417,6 +417,30 @@ impl TryFrom for types::PaymentsPreProcessingData complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: data.surcharge_details, + connector_transaction_id: None, + }) + } +} + +impl TryFrom for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(data: types::CompleteAuthorizeData) -> Result { + Ok(Self { + payment_method_data: data.payment_method_data, + amount: Some(data.amount), + email: data.email, + currency: Some(data.currency), + payment_method_type: None, + setup_mandate_details: data.setup_mandate_details, + capture_method: data.capture_method, + order_details: None, + router_return_url: None, + webhook_url: None, + complete_authorize_url: None, + browser_info: data.browser_info, + surcharge_details: None, + connector_transaction_id: data.connector_transaction_id, }) } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 44d8728fd4d2..2d52a145feae 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -6,7 +6,7 @@ use crate::{ errors::{self, ConnectorErrorExt, RouterResult}, payments::{self, access_token, helpers, transformers, PaymentData}, }, - routes::AppState, + routes::{metrics, AppState}, services, types::{self, api, domain}, utils::OptionExt, @@ -144,6 +144,76 @@ impl Feature Ok((request, true)) } + + async fn preprocessing_steps<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + ) -> RouterResult { + complete_authorize_preprocessing_steps(state, &self, true, connector).await + } +} + +pub async fn complete_authorize_preprocessing_steps( + state: &AppState, + router_data: &types::RouterData, + confirm: bool, + connector: &api::ConnectorData, +) -> RouterResult> { + if confirm { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let preprocessing_request_data = + types::PaymentsPreProcessingData::try_from(router_data.request.to_owned())?; + + let preprocessing_response_data: Result = + Err(types::ErrorResponse::default()); + + let preprocessing_router_data = + payments::helpers::router_data_type_conversion::<_, api::PreProcessing, _, _, _, _>( + router_data.clone(), + preprocessing_request_data, + preprocessing_response_data, + ); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &preprocessing_router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + + metrics::PREPROCESSING_STEPS_COUNT.add( + &metrics::CONTEXT, + 1, + &[ + metrics::request::add_attributes("connector", connector.connector_name.to_string()), + metrics::request::add_attributes( + "payment_method", + router_data.payment_method.to_string(), + ), + ], + ); + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data.request.to_owned(), + resp.response, + ); + + Ok(authorize_router_data) + } else { + Ok(router_data.clone()) + } } impl TryFrom for types::PaymentMethodTokenizationData { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f395c023128c..000bbb0fc00b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1428,6 +1428,7 @@ impl TryFrom> for types::PaymentsPreProce complete_authorize_url, browser_info, surcharge_details: payment_data.surcharge_details, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 8c9d030965c9..db126b81451d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -442,6 +442,7 @@ pub struct PaymentsPreProcessingData { pub complete_authorize_url: Option, pub surcharge_details: Option, pub browser_info: Option, + pub connector_transaction_id: Option, } #[derive(Debug, Clone)] From bd889c834dd5e201b055233016f7226fa2187aea Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:32:53 +0530 Subject: [PATCH 086/443] fix(connector): [Adyen] `ErrorHandling` in case of Balance Check for Gift Cards (#1976) --- crates/router/src/connector/adyen.rs | 155 ++++++------------ .../src/connector/adyen/transformers.rs | 46 ++++-- crates/router/src/consts.rs | 1 + crates/router/src/core/payments.rs | 11 ++ crates/router/src/core/payments/flows.rs | 1 - crates/router/src/types.rs | 2 +- .../.meta.json | 3 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 71 ++++++++ .../Payments - Create/request.json | 88 ++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++ .../Payments - Retrieve/request.json | 28 ++++ .../Payments - Retrieve/response.json | 1 + .../Payment Connector - Create/request.json | 12 ++ .../QuickStart/Payments - Create/request.json | 4 + .../.meta.json | 3 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 81 +++++++++ .../Payments - Create/request.json | 88 ++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++ .../Payments - Retrieve/request.json | 28 ++++ .../Payments - Retrieve/response.json | 1 + 26 files changed, 660 insertions(+), 120 deletions(-) create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index e101b796b8d4..ddd93bc289a9 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -14,11 +14,8 @@ use crate::{ configs::settings, connector::utils as connector_utils, consts, - core::{ - self, - errors::{self, CustomResult}, - }, - headers, logger, routes, + core::errors::{self, CustomResult}, + headers, logger, services::{ self, request::{self, Mask}, @@ -560,7 +557,6 @@ impl } } -#[async_trait::async_trait] impl services::ConnectorIntegration< api::Authorize, @@ -568,49 +564,6 @@ impl types::PaymentsResponseData, > for Adyen { - async fn execute_pretasks( - &self, - router_data: &mut types::PaymentsAuthorizeRouterData, - app_state: &routes::AppState, - ) -> CustomResult<(), errors::ConnectorError> { - match &router_data.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card_data) => { - match gift_card_data.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let integ: Box< - &(dyn services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > + Send - + Sync - + 'static), - > = Box::new(&Self); - - let authorize_data = &types::PaymentsBalanceRouterData::from(( - &router_data.to_owned(), - router_data.request.clone(), - )); - - let resp = services::execute_connector_processing_step( - app_state, - integ, - authorize_data, - core::payments::CallConnectorAction::Trigger, - None, - ) - .await?; - router_data.payment_method_balance = resp.payment_method_balance; - - Ok(()) - } - _ => Ok(()), - } - } - _ => Ok(()), - } - } - fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, @@ -667,7 +620,6 @@ impl req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - check_for_payment_method_balance(req)?; Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) @@ -725,28 +677,23 @@ impl } } +impl api::PaymentsPreProcessing for Adyen {} + impl services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, + api::PreProcessing, + types::PaymentsPreProcessingData, types::PaymentsResponseData, > for Adyen { fn get_headers( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> - where - Self: services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - { + ) -> CustomResult)>, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), - types::PaymentsBalanceType::get_content_type(self) + types::PaymentsPreProcessingType::get_content_type(self) .to_string() .into(), )]; @@ -757,7 +704,7 @@ impl fn get_url( &self, - _req: &types::PaymentsBalanceRouterData, + _req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult { Ok(format!( @@ -768,7 +715,7 @@ impl fn get_request_body( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; @@ -783,18 +730,20 @@ impl fn build_request( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsBalanceType::get_url(self, req, connectors)?) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) .attach_default_headers() - .headers(types::PaymentsBalanceType::get_headers( + .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsBalanceType::get_request_body( + .body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -803,19 +752,47 @@ impl fn handle_response( &self, - data: &types::PaymentsBalanceRouterData, + data: &types::PaymentsPreProcessingRouterData, res: types::Response, - ) -> CustomResult { + ) -> CustomResult { let response: adyen::AdyenBalanceResponse = res .response .parse_struct("AdyenBalanceResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + + let currency = match data.request.currency { + Some(currency) => currency, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + }; + let amount = match data.request.amount { + Some(amount) => amount, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + }; + + if response.balance.currency != currency || response.balance.value < amount { + Ok(types::RouterData { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(consts::LOW_BALANCE_ERROR_MESSAGE.to_string()), + status_code: res.status_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: None, + }), + ..data.clone() + }) + } else { + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } } fn get_error_response( @@ -1634,7 +1611,7 @@ impl api::IncomingWebhook for Adyen { .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(api::disputes::DisputePayload { amount: notif.amount.value.to_string(), - currency: notif.amount.currency, + currency: notif.amount.currency.to_string(), dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()), connector_dispute_id: notif.psp_reference, connector_reason: notif.reason, @@ -1646,27 +1623,3 @@ impl api::IncomingWebhook for Adyen { }) } } - -pub fn check_for_payment_method_balance( - req: &types::PaymentsAuthorizeRouterData, -) -> CustomResult<(), errors::ConnectorError> { - match &req.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card) => match gift_card.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let payment_method_balance = req - .payment_method_balance - .as_ref() - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - if payment_method_balance.currency != req.request.currency.to_string() - || payment_method_balance.amount < req.request.amount - { - Err(errors::ConnectorError::InSufficientBalanceInPaymentMethod.into()) - } else { - Ok(()) - } - } - _ => Ok(()), - }, - _ => Ok(()), - } -} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index a130ac50cc04..cfa601112677 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -213,8 +213,8 @@ pub struct AdyenBalanceRequest<'a> { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenBalanceResponse { - psp_reference: String, - balance: Amount, + pub psp_reference: String, + pub balance: Amount, } /// This implementation will be used only in Authorize, Automatic capture flow. @@ -397,8 +397,8 @@ pub enum ActionType { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Amount { - currency: String, - value: i64, + pub currency: storage_enums::Currency, + pub value: i64, } #[derive(Debug, Clone, Serialize)] @@ -1392,11 +1392,11 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> } } -impl<'a> TryFrom<&types::PaymentsBalanceRouterData> for AdyenBalanceRequest<'a> { +impl<'a> TryFrom<&types::PaymentsPreProcessingRouterData> for AdyenBalanceRequest<'a> { type Error = Error; - fn try_from(item: &types::PaymentsBalanceRouterData) -> Result { + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { let payment_method = match &item.request.payment_method_data { - payments::PaymentMethodData::GiftCard(gift_card_data) => { + Some(payments::PaymentMethodData::GiftCard(gift_card_data)) => { match gift_card_data.as_ref() { payments::GiftCardData::Givex(gift_card_data) => { let balance_pm = BalancePmData { @@ -1510,7 +1510,7 @@ fn get_channel_type(pm_type: &Option) -> Optio fn get_amount_data(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) -> Amount { Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), } } @@ -2857,12 +2857,24 @@ impl TryFrom> } } -impl TryFrom> - for types::PaymentsBalanceRouterData +impl + TryFrom< + types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = Error; fn try_from( - item: types::PaymentsBalanceResponseRouterData, + item: types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, ) -> Result { Ok(Self { response: Ok(types::PaymentsResponseData::TransactionResponse { @@ -3457,7 +3469,7 @@ impl TryFrom<&AdyenRouterData<&types::PaymentsCaptureRouterData>> for AdyenCaptu merchant_account: auth_type.merchant_account, reference, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), }, }) @@ -3547,7 +3559,7 @@ impl TryFrom<&AdyenRouterData<&types::RefundsRouterData>> for AdyenRefundR Ok(Self { merchant_account: auth_type.merchant_account, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.router_data.request.refund_amount, }, merchant_refund_reason: item.router_data.request.reason.clone(), @@ -3629,7 +3641,7 @@ pub struct AdyenAdditionalDataWH { #[derive(Debug, Deserialize)] pub struct AdyenAmountWH { pub value: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[derive(Clone, Debug, Deserialize, Serialize, strum::Display, PartialEq)] @@ -3955,7 +3967,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE )?; Ok(Self { amount: Amount { - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, value: item.amount.to_owned(), }, merchant_account: auth_type.merchant_account, @@ -4030,7 +4042,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC Ok(Self { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, recurring: RecurringContract { contract: Contract::Payout, @@ -4077,7 +4089,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF Ok(Self::Card(Box::new(PayoutFulfillCardRequest { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, card: get_payout_card_details(&item.router_data.get_payout_method_data()?) .map_or( diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 8937764409f8..4f19562c83ce 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,6 +27,7 @@ pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; +pub(crate) const LOW_BALANCE_ERROR_MESSAGE: &str = "Insufficient balance in the payment method"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; pub(crate) const CANNOT_CONTINUE_AUTH: &str = "Cannot continue with Authorization due to failed Liability Shift."; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 4fe7ea848008..8cfded8463eb 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1408,6 +1408,17 @@ where (router_data, should_continue_payment) } } + Some(api_models::payments::PaymentMethodData::GiftCard(_)) => { + if connector.connector_name == router_types::Connector::Adyen { + router_data = router_data.preprocessing_steps(state, connector).await?; + + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } Some(api_models::payments::PaymentMethodData::BankDebit(_)) => { if connector.connector_name == router_types::Connector::Gocardless { router_data = router_data.preprocessing_steps(state, connector).await?; diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index d983cd19bdb5..9be6f5905b8b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -832,7 +832,6 @@ impl default_imp_for_pre_processing_steps!( connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bambora, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index db126b81451d..cd37fbb549d9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -323,7 +323,7 @@ pub struct ApplePayCryptogramData { #[derive(Debug, Clone)] pub struct PaymentMethodBalance { pub amount: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[cfg(feature = "payouts")] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js new file mode 100644 index 000000000000..c48d8e2d054e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json new file mode 100644 index 000000000000..0915e9894bb6 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1100, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..0652a2d92fd4 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 592cff807510..fe25f6f5e682 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -190,6 +190,18 @@ } ] }, + { + "payment_method": "gift_card", + "payment_method_types": [ + { + "payment_method_type": "givex", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, { "payment_method": "bank_redirect", "payment_method_types": [ diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json index 8ac3ed14b0a7..ed9dbeaa9c49 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json @@ -43,6 +43,10 @@ "card_cvc": "7373" } }, + "routing": { + "type": "single", + "data": "adyen" + }, "billing": { "address": { "line1": "1467", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js new file mode 100644 index 000000000000..601f4f8fa7f5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js @@ -0,0 +1,81 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} + +// Response body should have error message as "Insufficient balance in the payment method" +if (jsonData?.error_message) { + pm.test( + "[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'", + function () { + pm.expect(jsonData.error_message).to.eql("Insufficient balance in the payment method"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json new file mode 100644 index 000000000000..11437ff57659 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 14100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 14100, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ff2099305d7a --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] From b3c51e6eb55c58adc024ee32b59c3910b2b72131 Mon Sep 17 00:00:00 2001 From: akshay-97 Date: Tue, 28 Nov 2023 18:58:46 +0530 Subject: [PATCH 087/443] refactor: Added min idle and max lifetime for database config (#2900) Co-authored-by: akshay.s Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/development.toml | 1 + crates/router/src/configs/defaults.rs | 2 ++ crates/router/src/configs/kms.rs | 2 ++ crates/router/src/configs/settings.rs | 4 ++++ crates/storage_impl/src/config.rs | 2 ++ crates/storage_impl/src/database/store.rs | 4 +++- 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/config/development.toml b/config/development.toml index 3d64a8791a1c..bcf561dd5857 100644 --- a/config/development.toml +++ b/config/development.toml @@ -20,6 +20,7 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 +min_idle = 2 [replica_database] username = "db_user" diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 2eddaf3084d7..a92e63d67639 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -30,6 +30,8 @@ impl Default for super::settings::Database { pool_size: 5, connection_timeout: 10, queue_strategy: Default::default(), + min_idle: None, + max_lifetime: None, } } } diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 205169fa291b..c2f159d16cf1 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -64,6 +64,8 @@ impl KmsDecrypt for settings::Database { pool_size: self.pool_size, connection_timeout: self.connection_timeout, queue_strategy: self.queue_strategy.into(), + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, }) } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index cc273f93ee9a..918ae6647eef 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -517,6 +517,8 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, } #[derive(Debug, Deserialize, Clone, Default)] @@ -548,6 +550,8 @@ impl From for storage_impl::config::Database { pool_size: val.pool_size, connection_timeout: val.connection_timeout, queue_strategy: val.queue_strategy.into(), + min_idle: val.min_idle, + max_lifetime: val.max_lifetime, } } } diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index ceed3da81b39..f53507831b11 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -10,4 +10,6 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: bb8::QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, } diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index a09f1b752561..c36575e37c97 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -88,8 +88,10 @@ pub async fn diesel_make_pg_pool( let manager = async_bb8_diesel::ConnectionManager::::new(database_url); let mut pool = bb8::Pool::builder() .max_size(database.pool_size) + .min_idle(database.min_idle) .queue_strategy(database.queue_strategy) - .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)); + .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)) + .max_lifetime(database.max_lifetime.map(std::time::Duration::from_secs)); if test_transaction { pool = pool.connection_customizer(Box::new(TestTransaction)); From cdbb3853cd44443f8487abc16a9ba5d99f22e475 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Tue, 28 Nov 2023 19:17:17 +0530 Subject: [PATCH 088/443] refactor(router): add openapi spec support for merchant_connector apis (#2997) --- crates/router/src/openapi.rs | 10 +- crates/router/src/routes/admin.rs | 2 +- openapi/openapi_spec.json | 253 ++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 6 deletions(-) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index ec38389cdc42..cfb0268a9f80 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -73,11 +73,11 @@ Never share your secret api keys. Keep them guarded and secure. // crate::routes::admin::retrieve_merchant_account, // crate::routes::admin::update_merchant_account, // crate::routes::admin::delete_merchant_account, - // crate::routes::admin::payment_connector_create, - // crate::routes::admin::payment_connector_retrieve, - // crate::routes::admin::payment_connector_list, - // crate::routes::admin::payment_connector_update, - // crate::routes::admin::payment_connector_delete, + crate::routes::admin::payment_connector_create, + crate::routes::admin::payment_connector_retrieve, + crate::routes::admin::payment_connector_list, + crate::routes::admin::payment_connector_update, + crate::routes::admin::payment_connector_delete, crate::routes::mandates::get_mandate, crate::routes::mandates::revoke_mandate, crate::routes::payments::payments_create, diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 0586faabbf76..ce6a2a97e28d 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -190,7 +190,7 @@ pub async fn delete_merchant_account( ) .await } -/// PaymentsConnectors - Create +/// Merchant Connector - Create /// /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." #[utoipa::path( diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 08f415782963..86dc053d2d77 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -129,6 +129,259 @@ ] } }, + "/accounts/{account_id}/connectors": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - List", + "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", + "operationId": "List all Merchant Connectors", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector list retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Create", + "description": "Merchant Connector - Create\n\nCreate a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", + "operationId": "Create a Merchant Connector", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/accounts/{account_id}/connectors/{connector_id}": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Retrieve", + "description": "Merchant Connector - Retrieve\n\nRetrieve Merchant Connector Details", + "operationId": "Retrieve a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Update", + "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc.", + "operationId": "Update a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Delete", + "description": "Merchant Connector - Delete\n\nDelete or Detach a Merchant Connector from Merchant Account", + "operationId": "Delete a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector Deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/customers": { "post": { "tags": [ From 1c5a9b5452afc33b18f45389bf3bdfd80820f476 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:09:50 +0530 Subject: [PATCH 089/443] fix: remove error propagation if card name not found in locker (#2998) --- crates/router/src/core/payments/helpers.rs | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 68b8128d7909..266792f98758 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1459,26 +1459,23 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker")?; - let name = card - .name_on_card - .get_required_value("name_on_card") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("card holder name was not saved in permanent locker")?; - - let name_on_card = if name.clone().expose().is_empty() { + let name_on_card = if let Some(name_on_card) = card.name_on_card.clone() { + if card.name_on_card.unwrap_or_default().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + } else { + Some(name_on_card) + } + } else { card_token_data .and_then(|token_data| token_data.card_holder_name.clone()) .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "card_holder_name", - })? - } else { - name }; let api_card = api::Card { card_number: card.card_number, - card_holder_name: name_on_card, + card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_cvc: card_cvc.unwrap_or_default(), From ff6a0dd0b515778b64a3e28ef905154eee85ec78 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:34:30 +0530 Subject: [PATCH 090/443] fix(core): Replace euclid enum with RoutableConnectors enum (#2994) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 2 + connector-template/mod.rs | 3 +- crates/api_models/src/enums.rs | 98 ----------------- crates/api_models/src/routing.rs | 67 +----------- crates/common_enums/Cargo.toml | 4 + crates/common_enums/src/enums.rs | 99 +++++++++++++++++ crates/euclid/src/enums.rs | 101 +----------------- crates/euclid/src/frontend/ast.rs | 8 +- crates/euclid/src/frontend/dir.rs | 8 +- crates/euclid/src/frontend/dir/enums.rs | 4 +- crates/euclid_wasm/Cargo.toml | 1 + crates/euclid_wasm/src/lib.rs | 12 ++- crates/kgraph_utils/Cargo.toml | 1 + crates/kgraph_utils/src/mca.rs | 7 +- crates/router/src/core/payments.rs | 2 +- .../src/core/payments/routing/transformers.rs | 75 +------------ crates/router/src/types/transformers.rs | 88 ++------------- scripts/add_connector.sh | 10 +- 18 files changed, 147 insertions(+), 443 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4a317d74f49..2ca33b6910a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2339,6 +2339,7 @@ name = "euclid_wasm" version = "0.1.0" dependencies = [ "api_models", + "common_enums", "currency_conversion", "euclid", "getrandom 0.2.10", @@ -3306,6 +3307,7 @@ name = "kgraph_utils" version = "0.1.0" dependencies = [ "api_models", + "common_enums", "criterion", "euclid", "masking", diff --git a/connector-template/mod.rs b/connector-template/mod.rs index e441b0e5879a..e9945a726a95 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -106,6 +106,7 @@ impl ConnectorCommon for {{project-name | downcase | pascal_case}} { message: response.message, reason: response.reason, attempt_status: None, + connector_transaction_id: None, }) } } @@ -485,7 +486,7 @@ impl api::IncomingWebhook for {{project-name | downcase | pascal_case}} { fn get_webhook_resource_object( &self, _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult, errors::ConnectorError> { Err(errors::ConnectorError::WebhooksNotImplemented).into_report() } } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ffefaa2ad2c4..535be4dfb159 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -147,104 +147,6 @@ impl Connector { } } -#[derive( - Clone, - Copy, - Debug, - Eq, - Hash, - PartialEq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumString, - strum::EnumIter, - strum::EnumVariantNames, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum RoutableConnectors { - #[cfg(feature = "dummy_connector")] - #[serde(rename = "phonypay")] - #[strum(serialize = "phonypay")] - DummyConnector1, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "fauxpay")] - #[strum(serialize = "fauxpay")] - DummyConnector2, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "pretendpay")] - #[strum(serialize = "pretendpay")] - DummyConnector3, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "stripe_test")] - #[strum(serialize = "stripe_test")] - DummyConnector4, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "adyen_test")] - #[strum(serialize = "adyen_test")] - DummyConnector5, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "checkout_test")] - #[strum(serialize = "checkout_test")] - DummyConnector6, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "paypal_test")] - #[strum(serialize = "paypal_test")] - DummyConnector7, - Aci, - Adyen, - Airwallex, - Authorizedotnet, - Bankofamerica, - Bitpay, - Bambora, - Bluesnap, - Boku, - Braintree, - Cashtocode, - Checkout, - Coinbase, - Cryptopay, - Cybersource, - Dlocal, - Fiserv, - Forte, - Globalpay, - Globepay, - Gocardless, - Helcim, - Iatapay, - Klarna, - Mollie, - Multisafepay, - Nexinets, - Nmi, - Noon, - Nuvei, - // Opayo, added as template code for future usage - Opennode, - // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage - Payme, - Paypal, - Payu, - Powertranz, - Prophetpay, - Rapyd, - Shift4, - Square, - Stax, - Stripe, - Trustpay, - // Tsys, - Tsys, - Volt, - Wise, - Worldline, - Worldpay, - Zen, -} - #[cfg(feature = "payouts")] #[derive( Clone, diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 47a44ea7443e..2236714da1d1 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -4,7 +4,6 @@ use common_utils::errors::ParsingError; use error_stack::IntoReport; use euclid::{ dssa::types::EuclidAnalysable, - enums as euclid_enums, frontend::{ ast, dir::{DirKeyKind, EuclidDirFilter}, @@ -287,71 +286,7 @@ impl From for RoutableChoiceSerde { impl From for ast::ConnectorChoice { fn from(value: RoutableConnectorChoice) -> Self { Self { - connector: match value.connector { - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector1 => euclid_enums::Connector::DummyConnector1, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector2 => euclid_enums::Connector::DummyConnector2, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector3 => euclid_enums::Connector::DummyConnector3, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector4 => euclid_enums::Connector::DummyConnector4, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector5 => euclid_enums::Connector::DummyConnector5, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector6 => euclid_enums::Connector::DummyConnector6, - #[cfg(feature = "dummy_connector")] - RoutableConnectors::DummyConnector7 => euclid_enums::Connector::DummyConnector7, - RoutableConnectors::Aci => euclid_enums::Connector::Aci, - RoutableConnectors::Adyen => euclid_enums::Connector::Adyen, - RoutableConnectors::Airwallex => euclid_enums::Connector::Airwallex, - RoutableConnectors::Authorizedotnet => euclid_enums::Connector::Authorizedotnet, - RoutableConnectors::Bambora => euclid_enums::Connector::Bambora, - RoutableConnectors::Bankofamerica => euclid_enums::Connector::Bankofamerica, - RoutableConnectors::Bitpay => euclid_enums::Connector::Bitpay, - RoutableConnectors::Bluesnap => euclid_enums::Connector::Bluesnap, - RoutableConnectors::Boku => euclid_enums::Connector::Boku, - RoutableConnectors::Braintree => euclid_enums::Connector::Braintree, - RoutableConnectors::Cashtocode => euclid_enums::Connector::Cashtocode, - RoutableConnectors::Checkout => euclid_enums::Connector::Checkout, - RoutableConnectors::Coinbase => euclid_enums::Connector::Coinbase, - RoutableConnectors::Cryptopay => euclid_enums::Connector::Cryptopay, - RoutableConnectors::Cybersource => euclid_enums::Connector::Cybersource, - RoutableConnectors::Dlocal => euclid_enums::Connector::Dlocal, - RoutableConnectors::Fiserv => euclid_enums::Connector::Fiserv, - RoutableConnectors::Forte => euclid_enums::Connector::Forte, - RoutableConnectors::Globalpay => euclid_enums::Connector::Globalpay, - RoutableConnectors::Globepay => euclid_enums::Connector::Globepay, - RoutableConnectors::Gocardless => euclid_enums::Connector::Gocardless, - RoutableConnectors::Helcim => euclid_enums::Connector::Helcim, - RoutableConnectors::Iatapay => euclid_enums::Connector::Iatapay, - RoutableConnectors::Klarna => euclid_enums::Connector::Klarna, - RoutableConnectors::Mollie => euclid_enums::Connector::Mollie, - RoutableConnectors::Multisafepay => euclid_enums::Connector::Multisafepay, - RoutableConnectors::Nexinets => euclid_enums::Connector::Nexinets, - RoutableConnectors::Nmi => euclid_enums::Connector::Nmi, - RoutableConnectors::Noon => euclid_enums::Connector::Noon, - RoutableConnectors::Nuvei => euclid_enums::Connector::Nuvei, - RoutableConnectors::Opennode => euclid_enums::Connector::Opennode, - RoutableConnectors::Payme => euclid_enums::Connector::Payme, - RoutableConnectors::Paypal => euclid_enums::Connector::Paypal, - RoutableConnectors::Payu => euclid_enums::Connector::Payu, - RoutableConnectors::Powertranz => euclid_enums::Connector::Powertranz, - RoutableConnectors::Prophetpay => euclid_enums::Connector::Prophetpay, - RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, - RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, - RoutableConnectors::Square => euclid_enums::Connector::Square, - RoutableConnectors::Stax => euclid_enums::Connector::Stax, - RoutableConnectors::Stripe => euclid_enums::Connector::Stripe, - RoutableConnectors::Trustpay => euclid_enums::Connector::Trustpay, - RoutableConnectors::Tsys => euclid_enums::Connector::Tsys, - RoutableConnectors::Volt => euclid_enums::Connector::Volt, - RoutableConnectors::Wise => euclid_enums::Connector::Wise, - RoutableConnectors::Worldline => euclid_enums::Connector::Worldline, - RoutableConnectors::Worldpay => euclid_enums::Connector::Worldpay, - RoutableConnectors::Zen => euclid_enums::Connector::Zen, - }, - + connector: value.connector, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: value.sub_label, } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 88628825ca64..cd061970bff3 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -7,6 +7,10 @@ rust-version.workspace = true readme = "README.md" license.workspace = true +[features] +default = ["dummy_connector"] +dummy_connector = [] + [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.160", features = ["derive"] } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 063e35933c43..3f343965130e 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -59,6 +59,105 @@ pub enum AttemptStatus { DeviceDataCollectionPending, } +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + strum::EnumIter, + strum::EnumVariantNames, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutableConnectors { + #[cfg(feature = "dummy_connector")] + #[serde(rename = "phonypay")] + #[strum(serialize = "phonypay")] + DummyConnector1, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "fauxpay")] + #[strum(serialize = "fauxpay")] + DummyConnector2, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "pretendpay")] + #[strum(serialize = "pretendpay")] + DummyConnector3, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "stripe_test")] + #[strum(serialize = "stripe_test")] + DummyConnector4, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "adyen_test")] + #[strum(serialize = "adyen_test")] + DummyConnector5, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "checkout_test")] + #[strum(serialize = "checkout_test")] + DummyConnector6, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "paypal_test")] + #[strum(serialize = "paypal_test")] + DummyConnector7, + Aci, + Adyen, + Airwallex, + Authorizedotnet, + Bankofamerica, + Bitpay, + Bambora, + Bluesnap, + Boku, + Braintree, + Cashtocode, + Checkout, + Coinbase, + Cryptopay, + Cybersource, + Dlocal, + Fiserv, + Forte, + Globalpay, + Globepay, + Gocardless, + Helcim, + Iatapay, + Klarna, + Mollie, + Multisafepay, + Nexinets, + Nmi, + Noon, + Nuvei, + // Opayo, added as template code for future usage + Opennode, + // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage + Payme, + Paypal, + Payu, + Powertranz, + Prophetpay, + Rapyd, + Shift4, + Square, + Stax, + Stripe, + Trustpay, + // Tsys, + Tsys, + Volt, + Wise, + Worldline, + Worldpay, + Zen, +} + impl AttemptStatus { pub fn is_terminal_status(self) -> bool { match self { diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index dc6d9f66a58f..68e081c7aa92 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -1,8 +1,7 @@ pub use common_enums::{ AuthenticationType, CaptureMethod, CardNetwork, Country, Currency, - FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, + FutureUsage as SetupFutureUsage, PaymentMethod, PaymentMethodType, RoutableConnectors, }; -use serde::{Deserialize, Serialize}; use strum::VariantNames; pub trait CollectVariants { @@ -24,6 +23,7 @@ macro_rules! collect_variants { pub(crate) use collect_variants; collect_variants!(PaymentMethod); +collect_variants!(RoutableConnectors); collect_variants!(PaymentType); collect_variants!(MandateType); collect_variants!(MandateAcceptanceType); @@ -33,105 +33,8 @@ collect_variants!(AuthenticationType); collect_variants!(CaptureMethod); collect_variants!(Currency); collect_variants!(Country); -collect_variants!(Connector); collect_variants!(SetupFutureUsage); -#[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - Hash, - Serialize, - Deserialize, - strum::Display, - strum::EnumVariantNames, - strum::EnumIter, - strum::EnumString, - frunk::LabelledGeneric, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum Connector { - #[cfg(feature = "dummy_connector")] - #[serde(rename = "phonypay")] - #[strum(serialize = "phonypay")] - DummyConnector1, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "fauxpay")] - #[strum(serialize = "fauxpay")] - DummyConnector2, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "pretendpay")] - #[strum(serialize = "pretendpay")] - DummyConnector3, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "stripe_test")] - #[strum(serialize = "stripe_test")] - DummyConnector4, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "adyen_test")] - #[strum(serialize = "adyen_test")] - DummyConnector5, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "checkout_test")] - #[strum(serialize = "checkout_test")] - DummyConnector6, - #[cfg(feature = "dummy_connector")] - #[serde(rename = "paypal_test")] - #[strum(serialize = "paypal_test")] - DummyConnector7, - Aci, - Adyen, - Airwallex, - Authorizedotnet, - Bambora, - Bankofamerica, - Bitpay, - Bluesnap, - Boku, - Braintree, - Cashtocode, - Checkout, - Coinbase, - Cryptopay, - Cybersource, - Dlocal, - Fiserv, - Forte, - Globalpay, - Globepay, - Gocardless, - Helcim, - Iatapay, - Klarna, - Mollie, - Multisafepay, - Nexinets, - Nmi, - Noon, - Nuvei, - Opennode, - Payme, - Paypal, - Payu, - Powertranz, - Prophetpay, - Rapyd, - Shift4, - Square, - Stax, - Stripe, - Trustpay, - Tsys, - Volt, - Wise, - Worldline, - Worldpay, - Zen, -} - #[derive( Clone, Debug, diff --git a/crates/euclid/src/frontend/ast.rs b/crates/euclid/src/frontend/ast.rs index 3adb06ab1873..0dad9b53c323 100644 --- a/crates/euclid/src/frontend/ast.rs +++ b/crates/euclid/src/frontend/ast.rs @@ -2,16 +2,14 @@ pub mod lowering; #[cfg(feature = "ast_parser")] pub mod parser; +use common_enums::RoutableConnectors; use serde::{Deserialize, Serialize}; -use crate::{ - enums::Connector, - types::{DataType, Metadata}, -}; +use crate::types::{DataType, Metadata}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct ConnectorChoice { - pub connector: Connector, + pub connector: RoutableConnectors, #[cfg(not(feature = "connector_choice_mca_id"))] pub sub_label: Option, } diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs index 7f2fc252d232..f8cef1f92955 100644 --- a/crates/euclid/src/frontend/dir.rs +++ b/crates/euclid/src/frontend/dir.rs @@ -13,7 +13,7 @@ macro_rules! dirval { (Connector = $name:ident) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, }, )) }; @@ -51,7 +51,7 @@ macro_rules! dirval { (Connector = $name:ident) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, sub_label: None, }, )) @@ -60,7 +60,7 @@ macro_rules! dirval { (Connector = ($name:ident, $sub_label:literal)) => { $crate::frontend::dir::DirValue::Connector(Box::new( $crate::frontend::ast::ConnectorChoice { - connector: $crate::frontend::dir::enums::Connector::$name, + connector: $crate::enums::RoutableConnectors::$name, sub_label: Some($sub_label.to_string()), }, )) @@ -464,7 +464,7 @@ impl DirKeyKind { .collect(), ), Self::Connector => Some( - enums::Connector::iter() + common_enums::RoutableConnectors::iter() .map(|connector| { DirValue::Connector(Box::new(ast::ConnectorChoice { connector, diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs index f049ad35328e..0b71f916d033 100644 --- a/crates/euclid/src/frontend/dir/enums.rs +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -2,9 +2,9 @@ use strum::VariantNames; use crate::enums::collect_variants; pub use crate::enums::{ - AuthenticationType, CaptureMethod, CardNetwork, Connector, Country, Country as BusinessCountry, + AuthenticationType, CaptureMethod, CardNetwork, Country, Country as BusinessCountry, Country as BillingCountry, Currency as PaymentCurrency, MandateAcceptanceType, MandateType, - PaymentMethod, PaymentType, SetupFutureUsage, + PaymentMethod, PaymentType, RoutableConnectors, SetupFutureUsage, }; #[derive( diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 47e349847ef7..8c96a7f67da2 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -20,6 +20,7 @@ api_models = { version = "0.1.0", path = "../api_models", package = "api_models" currency_conversion = { version = "0.1.0", path = "../currency_conversion" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +common_enums = { version = "0.1.0", path = "../common_enums" } # Third party crates getrandom = { version = "0.2.10", features = ["js"] } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index 48d9ac0d82a8..cab82f8ce411 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,6 +7,7 @@ use std::{ }; use api_models::{admin as admin_api, routing::ConnectorSelection}; +use common_enums::RoutableConnectors; use currency_conversion::{ conversion::convert as convert_currency, types as currency_conversion_types, }; @@ -17,7 +18,6 @@ use euclid::{ graph::{self, Memoization}, state_machine, truth, }, - enums, frontend::{ ast, dir::{self, enums as dir_enums}, @@ -61,8 +61,8 @@ pub fn convert_forex_value(amount: i64, from_currency: JsValue, to_currency: JsV .get() .ok_or("Forex Data not seeded") .err_to_js()?; - let from_currency: enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; - let to_currency: enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; + let from_currency: common_enums::Currency = serde_wasm_bindgen::from_value(from_currency)?; + let to_currency: common_enums::Currency = serde_wasm_bindgen::from_value(to_currency)?; let converted_amount = convert_currency(forex_data, from_currency, to_currency, amount) .map_err(|_| "conversion not possible for provided values") .err_to_js()?; @@ -80,7 +80,7 @@ pub fn seed_knowledge_graph(mcas: JsValue) -> JsResult { .iter() .map(|mca| { Ok::<_, strum::ParseError>(ast::ConnectorChoice { - connector: dir_enums::Connector::from_str(&mca.connector_name)?, + connector: RoutableConnectors::from_str(&mca.connector_name)?, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: mca.business_sub_label.clone(), }) @@ -183,7 +183,9 @@ pub fn run_program(program: JsValue, input: JsValue) -> JsResult { #[wasm_bindgen(js_name = getAllConnectors)] pub fn get_all_connectors() -> JsResult { - Ok(serde_wasm_bindgen::to_value(enums::Connector::VARIANTS)?) + Ok(serde_wasm_bindgen::to_value( + common_enums::RoutableConnectors::VARIANTS, + )?) } #[wasm_bindgen(js_name = getAllKeys)] diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index cd0adf0bc8af..44a73dae4d77 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -11,6 +11,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +common_enums = { version = "0.1.0", path = "../common_enums" } euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking/" } diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index deea51bd8808..0e224a8f3d9d 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -5,10 +5,7 @@ use api_models::{ }; use euclid::{ dssa::graph::{self, DomainIdentifier}, - frontend::{ - ast, - dir::{self, enums as dir_enums}, - }, + frontend::{ast, dir}, types::{NumValue, NumValueRefinement}, }; @@ -277,7 +274,7 @@ fn compile_merchant_connector_graph( builder: &mut graph::KnowledgeGraphBuilder<'_>, mca: admin_api::MerchantConnectorResponse, ) -> Result<(), KgraphError> { - let connector = dir_enums::Connector::from_str(&mca.connector_name) + let connector = common_enums::RoutableConnectors::from_str(&mca.connector_name) .map_err(|_| KgraphError::InvalidConnectorName(mca.connector_name.clone()))?; let mut agg_nodes: Vec<(graph::NodeId, graph::Relation)> = Vec::new(); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8cfded8463eb..db83dce487a6 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -81,7 +81,7 @@ pub async fn payments_operation_core( req: Req, call_connector_action: CallConnectorAction, auth_flow: services::AuthFlow, - eligible_connectors: Option>, + eligible_connectors: Option>, header_payload: HeaderPayload, ) -> RouterResult<( PaymentData, diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs index 5704f82f4983..b273f18f3fd8 100644 --- a/crates/router/src/core/payments/routing/transformers.rs +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -1,15 +1,15 @@ -use api_models::{self, enums as api_enums, routing as routing_types}; +use api_models::{self, routing as routing_types}; use diesel_models::enums as storage_enums; use euclid::{enums as dsl_enums, frontend::ast as dsl_ast}; -use crate::types::transformers::{ForeignFrom, ForeignInto}; +use crate::types::transformers::ForeignFrom; impl ForeignFrom for dsl_ast::ConnectorChoice { fn foreign_from(from: routing_types::RoutableConnectorChoice) -> Self { Self { // #[cfg(feature = "backwards_compatibility")] // choice_kind: from.choice_kind.foreign_into(), - connector: from.connector.foreign_into(), + connector: from.connector, #[cfg(not(feature = "connector_choice_mca_id"))] sub_label: from.sub_label, } @@ -52,72 +52,3 @@ impl ForeignFrom for dsl_enums::MandateType { } } } - -impl ForeignFrom for dsl_enums::Connector { - fn foreign_from(from: api_enums::RoutableConnectors) -> Self { - match from { - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector1 => Self::DummyConnector1, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector2 => Self::DummyConnector2, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector3 => Self::DummyConnector3, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector4 => Self::DummyConnector4, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector5 => Self::DummyConnector5, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector6 => Self::DummyConnector6, - #[cfg(feature = "dummy_connector")] - api_enums::RoutableConnectors::DummyConnector7 => Self::DummyConnector7, - api_enums::RoutableConnectors::Aci => Self::Aci, - api_enums::RoutableConnectors::Adyen => Self::Adyen, - api_enums::RoutableConnectors::Airwallex => Self::Airwallex, - api_enums::RoutableConnectors::Authorizedotnet => Self::Authorizedotnet, - api_enums::RoutableConnectors::Bambora => Self::Bambora, - api_enums::RoutableConnectors::Bankofamerica => Self::Bankofamerica, - api_enums::RoutableConnectors::Bitpay => Self::Bitpay, - api_enums::RoutableConnectors::Bluesnap => Self::Bluesnap, - api_enums::RoutableConnectors::Boku => Self::Boku, - api_enums::RoutableConnectors::Braintree => Self::Braintree, - api_enums::RoutableConnectors::Cashtocode => Self::Cashtocode, - api_enums::RoutableConnectors::Checkout => Self::Checkout, - api_enums::RoutableConnectors::Coinbase => Self::Coinbase, - api_enums::RoutableConnectors::Cryptopay => Self::Cryptopay, - api_enums::RoutableConnectors::Cybersource => Self::Cybersource, - api_enums::RoutableConnectors::Dlocal => Self::Dlocal, - api_enums::RoutableConnectors::Fiserv => Self::Fiserv, - api_enums::RoutableConnectors::Forte => Self::Forte, - api_enums::RoutableConnectors::Globalpay => Self::Globalpay, - api_enums::RoutableConnectors::Globepay => Self::Globepay, - api_enums::RoutableConnectors::Gocardless => Self::Gocardless, - api_enums::RoutableConnectors::Helcim => Self::Helcim, - api_enums::RoutableConnectors::Iatapay => Self::Iatapay, - api_enums::RoutableConnectors::Klarna => Self::Klarna, - api_enums::RoutableConnectors::Mollie => Self::Mollie, - api_enums::RoutableConnectors::Multisafepay => Self::Multisafepay, - api_enums::RoutableConnectors::Nexinets => Self::Nexinets, - api_enums::RoutableConnectors::Nmi => Self::Nmi, - api_enums::RoutableConnectors::Noon => Self::Noon, - api_enums::RoutableConnectors::Nuvei => Self::Nuvei, - api_enums::RoutableConnectors::Opennode => Self::Opennode, - api_enums::RoutableConnectors::Payme => Self::Payme, - api_enums::RoutableConnectors::Paypal => Self::Paypal, - api_enums::RoutableConnectors::Payu => Self::Payu, - api_enums::RoutableConnectors::Powertranz => Self::Powertranz, - api_enums::RoutableConnectors::Prophetpay => Self::Prophetpay, - api_enums::RoutableConnectors::Rapyd => Self::Rapyd, - api_enums::RoutableConnectors::Shift4 => Self::Shift4, - api_enums::RoutableConnectors::Square => Self::Square, - api_enums::RoutableConnectors::Stax => Self::Stax, - api_enums::RoutableConnectors::Stripe => Self::Stripe, - api_enums::RoutableConnectors::Trustpay => Self::Trustpay, - api_enums::RoutableConnectors::Tsys => Self::Tsys, - api_enums::RoutableConnectors::Volt => Self::Volt, - api_enums::RoutableConnectors::Wise => Self::Wise, - api_enums::RoutableConnectors::Worldline => Self::Worldline, - api_enums::RoutableConnectors::Worldpay => Self::Worldpay, - api_enums::RoutableConnectors::Zen => Self::Zen, - } - } -} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 5bd28db3c158..99096864a000 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -9,7 +9,6 @@ use common_utils::{ }; use diesel_models::enums as storage_enums; use error_stack::{IntoReport, ResultExt}; -use euclid::enums as dsl_enums; use masking::{ExposeInterface, PeekInterface}; use super::domain; @@ -174,25 +173,11 @@ impl ForeignFrom for api_models::payments::Manda } } -impl ForeignTryFrom for api_enums::RoutableConnectors { +impl ForeignTryFrom for common_enums::RoutableConnectors { type Error = error_stack::Report; fn foreign_try_from(from: api_enums::Connector) -> Result { Ok(match from { - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector1 => Self::DummyConnector1, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector2 => Self::DummyConnector2, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector3 => Self::DummyConnector3, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector4 => Self::DummyConnector4, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector5 => Self::DummyConnector5, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector6 => Self::DummyConnector6, - #[cfg(feature = "dummy_connector")] - api_enums::Connector::DummyConnector7 => Self::DummyConnector7, api_enums::Connector::Aci => Self::Aci, api_enums::Connector::Adyen => Self::Adyen, api_enums::Connector::Airwallex => Self::Airwallex, @@ -253,76 +238,21 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { api_enums::Connector::Worldline => Self::Worldline, api_enums::Connector::Worldpay => Self::Worldpay, api_enums::Connector::Zen => Self::Zen, - }) - } -} - -impl ForeignFrom for api_enums::RoutableConnectors { - fn foreign_from(from: dsl_enums::Connector) -> Self { - match from { #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector1 => Self::DummyConnector1, + api_enums::Connector::DummyConnector1 => Self::DummyConnector1, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector2 => Self::DummyConnector2, + api_enums::Connector::DummyConnector2 => Self::DummyConnector2, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector3 => Self::DummyConnector3, + api_enums::Connector::DummyConnector3 => Self::DummyConnector3, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector4 => Self::DummyConnector4, + api_enums::Connector::DummyConnector4 => Self::DummyConnector4, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector5 => Self::DummyConnector5, + api_enums::Connector::DummyConnector5 => Self::DummyConnector5, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector6 => Self::DummyConnector6, + api_enums::Connector::DummyConnector6 => Self::DummyConnector6, #[cfg(feature = "dummy_connector")] - dsl_enums::Connector::DummyConnector7 => Self::DummyConnector7, - dsl_enums::Connector::Aci => Self::Aci, - dsl_enums::Connector::Adyen => Self::Adyen, - dsl_enums::Connector::Airwallex => Self::Airwallex, - dsl_enums::Connector::Authorizedotnet => Self::Authorizedotnet, - dsl_enums::Connector::Bambora => Self::Bambora, - dsl_enums::Connector::Bankofamerica => Self::Bankofamerica, - dsl_enums::Connector::Bitpay => Self::Bitpay, - dsl_enums::Connector::Bluesnap => Self::Bluesnap, - dsl_enums::Connector::Boku => Self::Boku, - dsl_enums::Connector::Braintree => Self::Braintree, - dsl_enums::Connector::Cashtocode => Self::Cashtocode, - dsl_enums::Connector::Checkout => Self::Checkout, - dsl_enums::Connector::Coinbase => Self::Coinbase, - dsl_enums::Connector::Cryptopay => Self::Cryptopay, - dsl_enums::Connector::Cybersource => Self::Cybersource, - dsl_enums::Connector::Dlocal => Self::Dlocal, - dsl_enums::Connector::Fiserv => Self::Fiserv, - dsl_enums::Connector::Forte => Self::Forte, - dsl_enums::Connector::Globalpay => Self::Globalpay, - dsl_enums::Connector::Globepay => Self::Globepay, - dsl_enums::Connector::Gocardless => Self::Gocardless, - dsl_enums::Connector::Helcim => Self::Helcim, - dsl_enums::Connector::Iatapay => Self::Iatapay, - dsl_enums::Connector::Klarna => Self::Klarna, - dsl_enums::Connector::Mollie => Self::Mollie, - dsl_enums::Connector::Multisafepay => Self::Multisafepay, - dsl_enums::Connector::Nexinets => Self::Nexinets, - dsl_enums::Connector::Nmi => Self::Nmi, - dsl_enums::Connector::Noon => Self::Noon, - dsl_enums::Connector::Nuvei => Self::Nuvei, - dsl_enums::Connector::Opennode => Self::Opennode, - dsl_enums::Connector::Payme => Self::Payme, - dsl_enums::Connector::Paypal => Self::Paypal, - dsl_enums::Connector::Payu => Self::Payu, - dsl_enums::Connector::Powertranz => Self::Powertranz, - dsl_enums::Connector::Prophetpay => Self::Prophetpay, - dsl_enums::Connector::Rapyd => Self::Rapyd, - dsl_enums::Connector::Shift4 => Self::Shift4, - dsl_enums::Connector::Square => Self::Square, - dsl_enums::Connector::Stax => Self::Stax, - dsl_enums::Connector::Stripe => Self::Stripe, - dsl_enums::Connector::Trustpay => Self::Trustpay, - dsl_enums::Connector::Tsys => Self::Tsys, - dsl_enums::Connector::Volt => Self::Volt, - dsl_enums::Connector::Wise => Self::Wise, - dsl_enums::Connector::Worldline => Self::Worldline, - dsl_enums::Connector::Worldpay => Self::Worldpay, - dsl_enums::Connector::Zen => Self::Zen, - } + api_enums::Connector::DummyConnector7 => Self::DummyConnector7, + }) } } diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 9a30fe9d7573..7ed5e65151e1 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -45,7 +45,7 @@ cd $SCRIPT/.. # Remove template files if already created for this connector rm -rf $conn/$payment_gateway $conn/$payment_gateway.rs -git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs $src/core/admin.rs $src/core/payments/routing/transformers.rs $src/types/transformers.rs +git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/development.toml config/docker_compose.toml config/config.example.toml loadtest/config/development.toml crates/api_models/src/enums.rs crates/euclid/src/enums.rs crates/api_models/src/routing.rs $src/core/payments/flows.rs crates/common_enums/src/enums.rs $src/types/transformers.rs $src/core/admin.rs # Add enum for this connector in required places previous_connector='' @@ -61,14 +61,12 @@ sed -r -i'' -e "s/\"$previous_connector\",/\"$previous_connector\",\n \"${pa sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs sed -i '' -e "s/\(pub enum Connector {\)/\1\n\t${payment_gateway_camelcase},/" crates/euclid/src/enums.rs sed -i '' -e "s/\(match connector_name {\)/\1\n\t\tapi_enums::Connector::${payment_gateway_camelcase} => {${payment_gateway}::transformers::${payment_gateway_camelcase}AuthType::try_from(val)?;Ok(())}/" $src/core/admin.rs -sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::RoutableConnectors::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/core/payments/routing/transformers.rs -sed -i'' -e "s|dsl_enums::Connector::$previous_connector_camelcase \(.*\)|dsl_enums::Connector::$previous_connector_camelcase \1\n\t\t\tdsl_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs -sed -i'' -e "s|api_enums::Connector::$previous_connector_camelcase \(.*\)|api_enums::Connector::$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs -sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/api_models/src/enums.rs +sed -i'' -e "s/\(pub enum RoutableConnectors {\)/\1\n\t${payment_gateway_camelcase},/" crates/common_enums/src/enums.rs +sed -i'' -e "s|$previous_connector_camelcase \(.*\)|$previous_connector_camelcase \1\n\t\t\tapi_enums::Connector::${payment_gateway_camelcase} => Self::${payment_gateway_camelcase},|" $src/types/transformers.rs sed -i'' -e "s/^default_imp_for_\(.*\)/default_imp_for_\1\n\tconnector::${payment_gateway_camelcase},/" $src/core/payments/flows.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 config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e $src/core/admin.rs-e $src/core/payments/routing/transformers.rs-e $src/types/transformers.rs-e +rm $conn.rs-e $src/types/api.rs-e $src/configs/settings.rs-e config/development.toml-e config/docker_compose.toml-e config/config.example.toml-e loadtest/config/development.toml-e crates/api_models/src/enums.rs-e crates/euclid/src/enums.rs-e crates/api_models/src/routing.rs-e $src/core/payments/flows.rs-e crates/common_enums/src/enums.rs-e $src/types/transformers.rs-e $src/core/admin.rs-e cd $conn/ # Generate template files for the connector From 837480d935cce8cc35f07c5ccb3560285909bc52 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:44:55 +0530 Subject: [PATCH 091/443] feat(core): enable payment refund when payment is partially captured (#2991) Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- .../src/payments/payment_attempt.rs | 7 ++ .../src/query/payment_attempt.rs | 36 ++++++++++ crates/router/src/core/refunds.rs | 14 ++-- .../src/mock_db/payment_attempt.rs | 20 ++++++ .../src/payments/payment_attempt.rs | 72 +++++++++++++++++++ 5 files changed, 145 insertions(+), 4 deletions(-) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index a937c785902f..44aa48b142ad 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -36,6 +36,13 @@ pub trait PaymentAttemptInterface { storage_scheme: storage_enums::MerchantStorageScheme, ) -> error_stack::Result; + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> error_stack::Result; + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, diff --git a/crates/diesel_models/src/query/payment_attempt.rs b/crates/diesel_models/src/query/payment_attempt.rs index 4737233e3048..9e9195f5e0bb 100644 --- a/crates/diesel_models/src/query/payment_attempt.rs +++ b/crates/diesel_models/src/query/payment_attempt.rs @@ -120,6 +120,42 @@ impl PaymentAttempt { ) } + pub async fn find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + conn: &PgPooledConn, + payment_id: &str, + merchant_id: &str, + ) -> StorageResult { + // perform ordering on the application level instead of database level + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + Self, + >( + conn, + dsl::payment_id + .eq(payment_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and( + dsl::status + .eq(enums::AttemptStatus::Charged) + .or(dsl::status.eq(enums::AttemptStatus::PartialCharged)), + ), + None, + None, + None, + ) + .await? + .into_iter() + .fold( + Err(DatabaseError::NotFound).into_report(), + |acc, cur| match acc { + Ok(value) if value.modified_at > cur.modified_at => Ok(value), + _ => Ok(cur), + }, + ) + } + #[instrument(skip(conn))] pub async fn find_by_merchant_id_connector_txn_id( conn: &PgPooledConn, diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index aba6e9794e04..2d572cee9513 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -50,10 +50,16 @@ pub async fn refund_create_core( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; utils::when( - payment_intent.status != enums::IntentStatus::Succeeded, + !(payment_intent.status == enums::IntentStatus::Succeeded + || payment_intent.status == enums::IntentStatus::PartiallyCaptured), || { - Err(report!(errors::ApiErrorResponse::PaymentNotSucceeded) - .attach_printable("unable to refund for a unsuccessful payment intent")) + Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: "refund".into(), + field_name: "status".into(), + current_value: payment_intent.status.to_string(), + states: "succeeded, partially_captured".to_string() + }) + .attach_printable("unable to refund for a unsuccessful payment intent")) }, )?; @@ -75,7 +81,7 @@ pub async fn refund_create_core( })?; payment_attempt = db - .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( &req.payment_id, merchant_id, merchant_account.storage_scheme, diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index fe244b10325f..6137b444f963 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -205,4 +205,24 @@ impl PaymentAttemptInterface for MockDb { .cloned() .unwrap()) } + #[allow(clippy::unwrap_used)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + let payment_attempts = self.payment_attempts.lock().await; + + Ok(payment_attempts + .iter() + .find(|payment_attempt| { + payment_attempt.payment_id == payment_id + && payment_attempt.merchant_id == merchant_id + && (payment_attempt.status == storage_enums::AttemptStatus::PartialCharged + || payment_attempt.status == storage_enums::AttemptStatus::Charged) + }) + .cloned() + .unwrap()) + } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 06aacccc769d..e86119e41af6 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -115,6 +115,27 @@ impl PaymentAttemptInterface for RouterStore { .map(PaymentAttempt::from_storage_model) } + #[instrument(skip_all)] + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = pg_connection_read(self).await?; + DieselPaymentAttempt::find_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &conn, + payment_id, + merchant_id, + ) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(PaymentAttempt::from_storage_model) + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, @@ -618,6 +639,57 @@ impl PaymentAttemptInterface for KVRouterStore { } } + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let database_call = || { + self.router_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + }; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{merchant_id}_pid_{payment_id}"); + let pattern = "pa_*"; + + let redis_fut = async { + let kv_result = kv_wrapper::( + self, + KvOperation::::Scan(pattern), + key, + ) + .await? + .try_into_scan(); + kv_result.and_then(|mut payment_attempts| { + payment_attempts.sort_by(|a, b| b.modified_at.cmp(&a.modified_at)); + payment_attempts + .iter() + .find(|&pa| { + pa.status == api_models::enums::AttemptStatus::Charged + || pa.status == api_models::enums::AttemptStatus::PartialCharged + }) + .cloned() + .ok_or(error_stack::report!( + redis_interface::errors::RedisError::NotFound + )) + }) + }; + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await + } + } + } + async fn find_payment_attempt_by_merchant_id_connector_txn_id( &self, merchant_id: &str, From d63f6f7224f35018e7c707353508bbacc2baed5c Mon Sep 17 00:00:00 2001 From: ShivanshMathurJuspay <104988143+ShivanshMathurJuspay@users.noreply.github.com> Date: Tue, 28 Nov 2023 21:35:42 +0530 Subject: [PATCH 092/443] refactor(events): Adding changes to type of API events to Kafka (#2992) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/events/api_logs.rs | 10 ++++----- crates/router/src/services/authentication.rs | 22 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 3f0bf651c464..3f598e88394b 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -31,11 +31,11 @@ pub struct ApiEvent { status_code: i64, #[serde(flatten)] auth_type: AuthenticationType, - request: serde_json::Value, + request: String, user_agent: Option, ip_addr: Option, url_path: String, - response: Option, + response: Option, error: Option, #[serde(flatten)] event_type: ApiEventsType, @@ -59,12 +59,12 @@ impl ApiEvent { ) -> Self { Self { api_flow: api_flow.to_string(), - created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos(), + created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, request_id: request_id.as_hyphenated().to_string(), latency, status_code, - request, - response, + request: request.to_string(), + response: response.map(|resp| resp.to_string()), auth_type, error, ip_addr: http_req diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e24c7cebcb2a..b01e3762bfab 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -47,11 +47,11 @@ pub enum AuthenticationType { key_id: String, }, AdminApiKey, - MerchantJWT { + MerchantJwt { merchant_id: String, user_id: Option, }, - MerchantID { + MerchantId { merchant_id: String, }, PublishableKey { @@ -70,9 +70,9 @@ impl AuthenticationType { merchant_id, key_id: _, } - | Self::MerchantID { merchant_id } + | Self::MerchantId { merchant_id } | Self::PublishableKey { merchant_id } - | Self::MerchantJWT { + | Self::MerchantJwt { merchant_id, user_id: _, } @@ -352,7 +352,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantID { + AuthenticationType::MerchantId { merchant_id: auth.merchant_account.merchant_id.clone(), }, )) @@ -423,7 +423,7 @@ where Ok(( (), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -451,7 +451,7 @@ where org_id: payload.org_id, role_id: payload.role_id, }, - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -479,13 +479,13 @@ where let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.required_permission, permissions)?; - // Check if token has access to merchantID that has been requested through query param + // Check if token has access to MerchantId that has been requested through query param if payload.merchant_id != self.merchant_id { return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); } Ok(( (), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, @@ -549,7 +549,7 @@ where }; Ok(( auth.clone(), - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: auth.merchant_account.merchant_id.clone(), user_id: None, }, @@ -579,7 +579,7 @@ where org_id: payload.org_id, role_id: payload.role_id, }, - AuthenticationType::MerchantJWT { + AuthenticationType::MerchantJwt { merchant_id: payload.merchant_id, user_id: Some(payload.user_id), }, From af6b05c504b6fdbec7db77fa7f71535d7fea3e7a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:25:56 +0000 Subject: [PATCH 093/443] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 498 +++++++++++++++++- 1 file changed, 496 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 33aadeb6f970..400f04241c27 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -472,7 +472,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -735,7 +735,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1214,6 +1214,248 @@ { "name": "Happy Cases", "item": [ + { + "name": "Scenario22-Create Gift Card payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ @@ -10722,6 +10964,258 @@ { "name": "Variation Cases", "item": [ + { + "name": "Scenario10-Create Gift Card payment where it fails due to insufficient balance", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have error message as \"Insufficient balance in the payment method\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"Insufficient balance in the payment method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":14100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":14100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ From 1be197f6f0b664c281650caed4c2971e96360759 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:25:57 +0000 Subject: [PATCH 094/443] chore(version): v1.91.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b3abf1d5781..dfe703192a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.91.0 (2023-11-28) + +### Features + +- **core:** + - [Paypal] Add Preprocessing flow to CompleteAuthorize for Card 3DS Auth Verification ([#2757](https://github.com/juspay/hyperswitch/pull/2757)) ([`77fc92c`](https://github.com/juspay/hyperswitch/commit/77fc92c99a99aaf76d270ba5b981928183a05768)) + - Enable payment refund when payment is partially captured ([#2991](https://github.com/juspay/hyperswitch/pull/2991)) ([`837480d`](https://github.com/juspay/hyperswitch/commit/837480d935cce8cc35f07c5ccb3560285909bc52)) +- **currency_conversion:** Add currency conversion feature ([#2948](https://github.com/juspay/hyperswitch/pull/2948)) ([`c0116db`](https://github.com/juspay/hyperswitch/commit/c0116db271f6afc1b93c04705209bfc346228c68)) +- **payment_methods:** Receive `card_holder_name` in confirm flow when using token for payment ([#2982](https://github.com/juspay/hyperswitch/pull/2982)) ([`e7ad3a4`](https://github.com/juspay/hyperswitch/commit/e7ad3a4db8823f3ae8d381771739670d8350e6da)) + +### Bug Fixes + +- **connector:** [Adyen] `ErrorHandling` in case of Balance Check for Gift Cards ([#1976](https://github.com/juspay/hyperswitch/pull/1976)) ([`bd889c8`](https://github.com/juspay/hyperswitch/commit/bd889c834dd5e201b055233016f7226fa2187aea)) +- **core:** Replace euclid enum with RoutableConnectors enum ([#2994](https://github.com/juspay/hyperswitch/pull/2994)) ([`ff6a0dd`](https://github.com/juspay/hyperswitch/commit/ff6a0dd0b515778b64a3e28ef905154eee85ec78)) +- Remove error propagation if card name not found in locker ([#2998](https://github.com/juspay/hyperswitch/pull/2998)) ([`1c5a9b5`](https://github.com/juspay/hyperswitch/commit/1c5a9b5452afc33b18f45389bf3bdfd80820f476)) + +### Refactors + +- **events:** Adding changes to type of API events to Kafka ([#2992](https://github.com/juspay/hyperswitch/pull/2992)) ([`d63f6f7`](https://github.com/juspay/hyperswitch/commit/d63f6f7224f35018e7c707353508bbacc2baed5c)) +- **masking:** Use empty enums as masking:Strategy types ([#2874](https://github.com/juspay/hyperswitch/pull/2874)) ([`0e66b1b`](https://github.com/juspay/hyperswitch/commit/0e66b1b5dcce6dd87c9d743c9eb73d0cd8e330b2)) +- **router:** Add openapi spec support for merchant_connector apis ([#2997](https://github.com/juspay/hyperswitch/pull/2997)) ([`cdbb385`](https://github.com/juspay/hyperswitch/commit/cdbb3853cd44443f8487abc16a9ba5d99f22e475)) +- Added min idle and max lifetime for database config ([#2900](https://github.com/juspay/hyperswitch/pull/2900)) ([`b3c51e6`](https://github.com/juspay/hyperswitch/commit/b3c51e6eb55c58adc024ee32b59c3910b2b72131)) + +### Testing + +- **postman:** Update postman collection files ([`af6b05c`](https://github.com/juspay/hyperswitch/commit/af6b05c504b6fdbec7db77fa7f71535d7fea3e7a)) + +**Full Changelog:** [`v1.90.0...v1.91.0`](https://github.com/juspay/hyperswitch/compare/v1.90.0...v1.91.0) + +- - - + + ## 1.90.0 (2023-11-27) ### Features From 1c2f35af92608fca5836448710eca9f9c23a776a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:39:42 +0530 Subject: [PATCH 095/443] chore(deps): bump openssl from 0.10.57 to 0.10.60 (#3004) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- crates/router/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ca33b6910a0..a16dde18e83e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3861,9 +3861,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ "bitflags 2.4.0", "cfg-if 1.0.0", @@ -3893,9 +3893,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" dependencies = [ "cc", "libc", diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index f0316d69249e..a5f8b2f6b847 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -68,7 +68,7 @@ mime = "0.3.17" nanoid = "0.4.0" num_cpus = "1.15.0" once_cell = "1.18.0" -openssl = "0.10.55" +openssl = "0.10.60" qrcode = "0.12.0" rand = "0.8.5" rand_chacha = "0.3.1" From bb593ab0cd1a30190b6c305f2432de83ac7fde93 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:42:36 +0530 Subject: [PATCH 096/443] fix: remove `dummy_connector` from `default` features in `common_enums` (#3005) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- crates/api_models/Cargo.toml | 2 +- crates/common_enums/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 73c2d673c972..cb2e243745de 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -14,7 +14,7 @@ connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] backwards_compatibility = ["connector_choice_bcompat"] connector_choice_mca_id = ["euclid/connector_choice_mca_id"] -dummy_connector = ["euclid/dummy_connector"] +dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index cd061970bff3..72d9f6bb0bb1 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -8,7 +8,6 @@ readme = "README.md" license.workspace = true [features] -default = ["dummy_connector"] dummy_connector = [] [dependencies] From 5c32b3739e2c5895fe7f5cf8cc92f917c2639eac Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:47:16 +0530 Subject: [PATCH 097/443] fix: remove error propagation if card name not found in locker in case of temporary token (#3006) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/core/payments/helpers.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 266792f98758..7a8a76e1123a 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1381,18 +1381,19 @@ pub async fn retrieve_payment_method_with_temporary_token( let name_on_card = if card.card_holder_name.clone().expose().is_empty() { card_token_data - .and_then(|token_data| { + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .map(|name_on_card| { is_card_updated = true; - token_data.card_holder_name.clone() + name_on_card }) - .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "card_holder_name", - })? } else { - card.card_holder_name.clone() + Some(card.card_holder_name.clone()) }; - updated_card.card_holder_name = name_on_card; + + if let Some(name_on_card) = name_on_card { + updated_card.card_holder_name = name_on_card; + } if let Some(cvc) = card_cvc { is_card_updated = true; From d289524869f0c3835db9cf90d57ebedf560e0291 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:54:16 +0530 Subject: [PATCH 098/443] fix: few fields were not getting updated in apply_changeset function (#3002) --- crates/diesel_models/src/business_profile.rs | 46 ++++--- crates/diesel_models/src/capture.rs | 21 +++- crates/diesel_models/src/payment_attempt.rs | 123 +++++++++++-------- crates/diesel_models/src/payment_intent.rs | 91 ++++++++------ crates/diesel_models/src/refund.rs | 32 +++-- 5 files changed, 190 insertions(+), 123 deletions(-) diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 1f6c4f604958..700104aaaecc 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -103,25 +103,39 @@ impl From for BusinessProfile { impl BusinessProfileUpdateInternal { pub fn apply_changeset(self, source: BusinessProfile) -> BusinessProfile { + let Self { + profile_name, + modified_at: _, + return_url, + enable_payment_response_hash, + payment_response_hash_key, + redirect_to_merchant_with_http_post, + webhook_details, + metadata, + routing_algorithm, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + is_recon_enabled, + applepay_verified_domains, + } = self; BusinessProfile { - profile_name: self.profile_name.unwrap_or(source.profile_name), - modified_at: self.modified_at.unwrap_or(source.modified_at), - return_url: self.return_url, - enable_payment_response_hash: self - .enable_payment_response_hash + profile_name: profile_name.unwrap_or(source.profile_name), + modified_at: common_utils::date_time::now(), + return_url, + enable_payment_response_hash: enable_payment_response_hash .unwrap_or(source.enable_payment_response_hash), - payment_response_hash_key: self.payment_response_hash_key, - redirect_to_merchant_with_http_post: self - .redirect_to_merchant_with_http_post + payment_response_hash_key, + redirect_to_merchant_with_http_post: redirect_to_merchant_with_http_post .unwrap_or(source.redirect_to_merchant_with_http_post), - webhook_details: self.webhook_details, - metadata: self.metadata, - routing_algorithm: self.routing_algorithm, - intent_fulfillment_time: self.intent_fulfillment_time, - frm_routing_algorithm: self.frm_routing_algorithm, - payout_routing_algorithm: self.payout_routing_algorithm, - is_recon_enabled: self.is_recon_enabled.unwrap_or(source.is_recon_enabled), - applepay_verified_domains: self.applepay_verified_domains, + webhook_details, + metadata, + routing_algorithm, + intent_fulfillment_time, + frm_routing_algorithm, + payout_routing_algorithm, + is_recon_enabled: is_recon_enabled.unwrap_or(source.is_recon_enabled), + applepay_verified_domains, ..source } } diff --git a/crates/diesel_models/src/capture.rs b/crates/diesel_models/src/capture.rs index 30eee900cff1..adc313ca3dde 100644 --- a/crates/diesel_models/src/capture.rs +++ b/crates/diesel_models/src/capture.rs @@ -83,13 +83,24 @@ pub struct CaptureUpdateInternal { impl CaptureUpdate { pub fn apply_changeset(self, source: Capture) -> Capture { - let capture_update: CaptureUpdateInternal = self.into(); + let CaptureUpdateInternal { + status, + error_message, + error_code, + error_reason, + modified_at: _, + connector_capture_id, + connector_response_reference_id, + } = self.into(); Capture { - status: capture_update.status.unwrap_or(source.status), - error_message: capture_update.error_message.or(source.error_message), - error_code: capture_update.error_code.or(source.error_code), - error_reason: capture_update.error_reason.or(source.error_reason), + status: status.unwrap_or(source.status), + error_message: error_message.or(source.error_message), + error_code: error_code.or(source.error_code), + error_reason: error_reason.or(source.error_reason), modified_at: common_utils::date_time::now(), + connector_capture_id: connector_capture_id.or(source.connector_capture_id), + connector_response_reference_id: connector_response_reference_id + .or(source.connector_response_reference_id), ..source } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 9cc6632c638e..216801fa8fb1 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -314,60 +314,83 @@ pub struct PaymentAttemptUpdateInternal { impl PaymentAttemptUpdate { pub fn apply_changeset(self, source: PaymentAttempt) -> PaymentAttempt { - let pa_update: PaymentAttemptUpdateInternal = self.into(); + let PaymentAttemptUpdateInternal { + amount, + currency, + status, + connector_transaction_id, + amount_to_capture, + connector, + authentication_type, + payment_method, + error_message, + payment_method_id, + cancellation_reason, + modified_at: _, + mandate_id, + browser_info, + payment_token, + error_code, + connector_metadata, + payment_method_data, + payment_method_type, + payment_experience, + business_sub_label, + straight_through_algorithm, + preprocessing_step_id, + error_reason, + capture_method, + connector_response_reference_id, + multiple_capture_count, + surcharge_amount, + tax_amount, + amount_capturable, + updated_by, + merchant_connector_id, + authentication_data, + encoded_data, + unified_code, + unified_message, + } = self.into(); PaymentAttempt { - amount: pa_update.amount.unwrap_or(source.amount), - currency: pa_update.currency.or(source.currency), - status: pa_update.status.unwrap_or(source.status), - connector_transaction_id: pa_update - .connector_transaction_id - .or(source.connector_transaction_id), - amount_to_capture: pa_update.amount_to_capture.or(source.amount_to_capture), - connector: pa_update.connector.or(source.connector), - authentication_type: pa_update.authentication_type.or(source.authentication_type), - payment_method: pa_update.payment_method.or(source.payment_method), - error_message: pa_update.error_message.unwrap_or(source.error_message), - payment_method_id: pa_update - .payment_method_id - .unwrap_or(source.payment_method_id), - cancellation_reason: pa_update.cancellation_reason.or(source.cancellation_reason), + amount: amount.unwrap_or(source.amount), + currency: currency.or(source.currency), + status: status.unwrap_or(source.status), + connector_transaction_id: connector_transaction_id.or(source.connector_transaction_id), + amount_to_capture: amount_to_capture.or(source.amount_to_capture), + connector: connector.or(source.connector), + authentication_type: authentication_type.or(source.authentication_type), + payment_method: payment_method.or(source.payment_method), + error_message: error_message.unwrap_or(source.error_message), + payment_method_id: payment_method_id.unwrap_or(source.payment_method_id), + cancellation_reason: cancellation_reason.or(source.cancellation_reason), modified_at: common_utils::date_time::now(), - mandate_id: pa_update.mandate_id.or(source.mandate_id), - browser_info: pa_update.browser_info.or(source.browser_info), - payment_token: pa_update.payment_token.or(source.payment_token), - error_code: pa_update.error_code.unwrap_or(source.error_code), - connector_metadata: pa_update.connector_metadata.or(source.connector_metadata), - payment_method_data: pa_update.payment_method_data.or(source.payment_method_data), - payment_method_type: pa_update.payment_method_type.or(source.payment_method_type), - payment_experience: pa_update.payment_experience.or(source.payment_experience), - business_sub_label: pa_update.business_sub_label.or(source.business_sub_label), - straight_through_algorithm: pa_update - .straight_through_algorithm + mandate_id: mandate_id.or(source.mandate_id), + browser_info: browser_info.or(source.browser_info), + payment_token: payment_token.or(source.payment_token), + error_code: error_code.unwrap_or(source.error_code), + connector_metadata: connector_metadata.or(source.connector_metadata), + payment_method_data: payment_method_data.or(source.payment_method_data), + payment_method_type: payment_method_type.or(source.payment_method_type), + payment_experience: payment_experience.or(source.payment_experience), + business_sub_label: business_sub_label.or(source.business_sub_label), + straight_through_algorithm: straight_through_algorithm .or(source.straight_through_algorithm), - preprocessing_step_id: pa_update - .preprocessing_step_id - .or(source.preprocessing_step_id), - error_reason: pa_update.error_reason.unwrap_or(source.error_reason), - capture_method: pa_update.capture_method.or(source.capture_method), - connector_response_reference_id: pa_update - .connector_response_reference_id + preprocessing_step_id: preprocessing_step_id.or(source.preprocessing_step_id), + error_reason: error_reason.unwrap_or(source.error_reason), + capture_method: capture_method.or(source.capture_method), + connector_response_reference_id: connector_response_reference_id .or(source.connector_response_reference_id), - multiple_capture_count: pa_update - .multiple_capture_count - .or(source.multiple_capture_count), - surcharge_amount: pa_update.surcharge_amount.or(source.surcharge_amount), - tax_amount: pa_update.tax_amount.or(source.tax_amount), - amount_capturable: pa_update - .amount_capturable - .unwrap_or(source.amount_capturable), - updated_by: pa_update.updated_by, - merchant_connector_id: pa_update - .merchant_connector_id - .or(source.merchant_connector_id), - authentication_data: pa_update.authentication_data.or(source.authentication_data), - encoded_data: pa_update.encoded_data.or(source.encoded_data), - unified_code: pa_update.unified_code.unwrap_or(source.unified_code), - unified_message: pa_update.unified_message.unwrap_or(source.unified_message), + multiple_capture_count: multiple_capture_count.or(source.multiple_capture_count), + surcharge_amount: surcharge_amount.or(source.surcharge_amount), + tax_amount: tax_amount.or(source.tax_amount), + amount_capturable: amount_capturable.unwrap_or(source.amount_capturable), + updated_by, + merchant_connector_id: merchant_connector_id.or(source.merchant_connector_id), + authentication_data: authentication_data.or(source.authentication_data), + encoded_data: encoded_data.or(source.encoded_data), + unified_code: unified_code.unwrap_or(source.unified_code), + unified_message: unified_message.unwrap_or(source.unified_message), ..source } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 2ffa857026ba..b6ff4fcf8d8d 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -217,50 +217,61 @@ pub struct PaymentIntentUpdateInternal { impl PaymentIntentUpdate { pub fn apply_changeset(self, source: PaymentIntent) -> PaymentIntent { - let internal_update: PaymentIntentUpdateInternal = self.into(); + let PaymentIntentUpdateInternal { + amount, + currency, + status, + amount_captured, + customer_id, + return_url, + setup_future_usage, + off_session, + metadata, + billing_address_id, + shipping_address_id, + modified_at: _, + active_attempt_id, + business_country, + business_label, + description, + statement_descriptor_name, + statement_descriptor_suffix, + order_details, + attempt_count, + profile_id, + merchant_decision, + payment_confirm_source, + updated_by, + surcharge_applicable, + } = self.into(); PaymentIntent { - amount: internal_update.amount.unwrap_or(source.amount), - currency: internal_update.currency.or(source.currency), - status: internal_update.status.unwrap_or(source.status), - amount_captured: internal_update.amount_captured.or(source.amount_captured), - customer_id: internal_update.customer_id.or(source.customer_id), - return_url: internal_update.return_url.or(source.return_url), - setup_future_usage: internal_update - .setup_future_usage - .or(source.setup_future_usage), - off_session: internal_update.off_session.or(source.off_session), - metadata: internal_update.metadata.or(source.metadata), - billing_address_id: internal_update - .billing_address_id - .or(source.billing_address_id), - shipping_address_id: internal_update - .shipping_address_id - .or(source.shipping_address_id), + amount: amount.unwrap_or(source.amount), + currency: currency.or(source.currency), + status: status.unwrap_or(source.status), + amount_captured: amount_captured.or(source.amount_captured), + customer_id: customer_id.or(source.customer_id), + return_url: return_url.or(source.return_url), + setup_future_usage: setup_future_usage.or(source.setup_future_usage), + off_session: off_session.or(source.off_session), + metadata: metadata.or(source.metadata), + billing_address_id: billing_address_id.or(source.billing_address_id), + shipping_address_id: shipping_address_id.or(source.shipping_address_id), modified_at: common_utils::date_time::now(), - active_attempt_id: internal_update - .active_attempt_id - .unwrap_or(source.active_attempt_id), - business_country: internal_update.business_country.or(source.business_country), - business_label: internal_update.business_label.or(source.business_label), - description: internal_update.description.or(source.description), - statement_descriptor_name: internal_update - .statement_descriptor_name + active_attempt_id: active_attempt_id.unwrap_or(source.active_attempt_id), + business_country: business_country.or(source.business_country), + business_label: business_label.or(source.business_label), + description: description.or(source.description), + statement_descriptor_name: statement_descriptor_name .or(source.statement_descriptor_name), - statement_descriptor_suffix: internal_update - .statement_descriptor_suffix + statement_descriptor_suffix: statement_descriptor_suffix .or(source.statement_descriptor_suffix), - order_details: internal_update.order_details.or(source.order_details), - attempt_count: internal_update - .attempt_count - .unwrap_or(source.attempt_count), - profile_id: internal_update.profile_id.or(source.profile_id), - merchant_decision: internal_update - .merchant_decision - .or(source.merchant_decision), - payment_confirm_source: internal_update - .payment_confirm_source - .or(source.payment_confirm_source), - updated_by: internal_update.updated_by, + order_details: order_details.or(source.order_details), + attempt_count: attempt_count.unwrap_or(source.attempt_count), + profile_id: profile_id.or(source.profile_id), + merchant_decision: merchant_decision.or(source.merchant_decision), + payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), + updated_by, + surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), ..source } } diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 62aec3fb27d8..bb805fb646c5 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -202,19 +202,27 @@ impl From for RefundUpdateInternal { impl RefundUpdate { pub fn apply_changeset(self, source: Refund) -> Refund { - let pa_update: RefundUpdateInternal = self.into(); + let RefundUpdateInternal { + connector_refund_id, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + metadata, + refund_reason, + refund_error_code, + updated_by, + } = self.into(); Refund { - connector_refund_id: pa_update.connector_refund_id.or(source.connector_refund_id), - refund_status: pa_update.refund_status.unwrap_or(source.refund_status), - sent_to_gateway: pa_update.sent_to_gateway.unwrap_or(source.sent_to_gateway), - refund_error_message: pa_update - .refund_error_message - .or(source.refund_error_message), - refund_error_code: pa_update.refund_error_code.or(source.refund_error_code), - refund_arn: pa_update.refund_arn.or(source.refund_arn), - metadata: pa_update.metadata.or(source.metadata), - refund_reason: pa_update.refund_reason.or(source.refund_reason), - updated_by: pa_update.updated_by, + connector_refund_id: connector_refund_id.or(source.connector_refund_id), + refund_status: refund_status.unwrap_or(source.refund_status), + sent_to_gateway: sent_to_gateway.unwrap_or(source.sent_to_gateway), + refund_error_message: refund_error_message.or(source.refund_error_message), + refund_error_code: refund_error_code.or(source.refund_error_code), + refund_arn: refund_arn.or(source.refund_arn), + metadata: metadata.or(source.metadata), + refund_reason: refund_reason.or(source.refund_reason), + updated_by, ..source } } From 37ab392488350c22d1d1352edc90f46af25d40be Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:44:23 +0000 Subject: [PATCH 099/443] chore(version): v1.91.1 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe703192a3a..5a63dcc2cae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.91.1 (2023-11-29) + +### Bug Fixes + +- Remove `dummy_connector` from `default` features in `common_enums` ([#3005](https://github.com/juspay/hyperswitch/pull/3005)) ([`bb593ab`](https://github.com/juspay/hyperswitch/commit/bb593ab0cd1a30190b6c305f2432de83ac7fde93)) +- Remove error propagation if card name not found in locker in case of temporary token ([#3006](https://github.com/juspay/hyperswitch/pull/3006)) ([`5c32b37`](https://github.com/juspay/hyperswitch/commit/5c32b3739e2c5895fe7f5cf8cc92f917c2639eac)) +- Few fields were not getting updated in apply_changeset function ([#3002](https://github.com/juspay/hyperswitch/pull/3002)) ([`d289524`](https://github.com/juspay/hyperswitch/commit/d289524869f0c3835db9cf90d57ebedf560e0291)) + +### Miscellaneous Tasks + +- **deps:** Bump openssl from 0.10.57 to 0.10.60 ([#3004](https://github.com/juspay/hyperswitch/pull/3004)) ([`1c2f35a`](https://github.com/juspay/hyperswitch/commit/1c2f35af92608fca5836448710eca9f9c23a776a)) + +**Full Changelog:** [`v1.91.0...v1.91.1`](https://github.com/juspay/hyperswitch/compare/v1.91.0...v1.91.1) + +- - - + + ## 1.91.0 (2023-11-28) ### Features From 5f5e895f638701a0e6ab3deea9101ef39033dd16 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:12:12 +0530 Subject: [PATCH 100/443] feat(ses_email): add email services to hyperswitch (#2977) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- Cargo.lock | 45 +++ config/config.example.toml | 17 +- config/development.toml | 10 +- crates/external_services/Cargo.toml | 3 + crates/external_services/src/email.rs | 194 ++++++----- crates/external_services/src/email/ses.rs | 257 +++++++++++++++ crates/router/Cargo.toml | 2 +- crates/router/src/consts.rs | 2 + crates/router/src/core/user.rs | 22 ++ crates/router/src/routes/app.rs | 20 +- crates/router/src/services.rs | 3 + crates/router/src/services/email.rs | 1 + .../src/services/email/assets/invite.html | 243 ++++++++++++++ .../src/services/email/assets/magic_link.html | 260 +++++++++++++++ .../email/assets/recon_activated.html | 309 ++++++++++++++++++ .../src/services/email/assets/reset.html | 229 +++++++++++++ .../src/services/email/assets/verify.html | 253 ++++++++++++++ crates/router/src/services/email/types.rs | 80 +++++ 18 files changed, 1857 insertions(+), 93 deletions(-) create mode 100644 crates/external_services/src/email/ses.rs create mode 100644 crates/router/src/services/email.rs create mode 100644 crates/router/src/services/email/assets/invite.html create mode 100644 crates/router/src/services/email/assets/magic_link.html create mode 100644 crates/router/src/services/email/assets/recon_activated.html create mode 100644 crates/router/src/services/email/assets/reset.html create mode 100644 crates/router/src/services/email/assets/verify.html create mode 100644 crates/router/src/services/email/types.rs diff --git a/Cargo.lock b/Cargo.lock index a16dde18e83e..96bdcff3f86e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2366,11 +2366,14 @@ dependencies = [ "aws-config", "aws-sdk-kms", "aws-sdk-sesv2", + "aws-sdk-sts", "aws-smithy-client", "base64 0.21.4", "common_utils", "dyn-clone", "error-stack", + "hyper", + "hyper-proxy", "masking", "once_cell", "router_env", @@ -2867,6 +2870,30 @@ dependencies = [ "hashbrown 0.14.1", ] +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.4", + "bytes 1.5.0", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -2994,6 +3021,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-proxy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" +dependencies = [ + "bytes 1.5.0", + "futures 0.3.28", + "headers", + "http", + "hyper", + "hyper-tls", + "native-tls", + "tokio 1.32.0", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.23.2" diff --git a/config/config.example.toml b/config/config.example.toml index 0b8730ca114a..d935a4e7f20d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -322,9 +322,17 @@ region = "" # The AWS region used by the KMS SDK for decrypting data. # EmailClient configuration. Only applicable when the `email` feature flag is enabled. [email] -from_email = "notify@example.com" # Sender email -aws_region = "" # AWS region used by AWS SES -base_url = "" # Base url used when adding links that should redirect to self +sender_email = "example@example.com" # Sender email +aws_region = "" # AWS region used by AWS SES +base_url = "" # Base url used when adding links that should redirect to self +allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email +active_email_client = "SES" # The currently active email client + +# Configuration for aws ses, applicable when the active email client is SES +[email.aws_ses] +email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails +sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session. + #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] @@ -427,9 +435,6 @@ credit = { currency = "USD" } debit = { currency = "USD" } ach = { currency = "USD" } -[pm_filters.stripe] -cashapp = { country = "US", currency = "USD" } - [pm_filters.prophetpay] card_redirect = { currency = "USD" } diff --git a/config/development.toml b/config/development.toml index bcf561dd5857..f2620bd37135 100644 --- a/config/development.toml +++ b/config/development.toml @@ -212,9 +212,15 @@ disabled = false consumer_group = "SCHEDULER_GROUP" [email] -from_email = "notify@example.com" +sender_email = "example@example.com" aws_region = "" -base_url = "" +base_url = "http://localhost:8080" +allowed_unverified_days = 1 +active_email_client = "SES" + +[email.aws_ses] +email_role_arn = "" +sts_role_session_name = "" [bank_config.eps] stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" } diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 4700c2a81d75..54a636a382b2 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -16,6 +16,7 @@ async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } aws-sdk-kms = { version = "0.28.0", optional = true } aws-sdk-sesv2 = "0.28.0" +aws-sdk-sts = "0.28.0" aws-smithy-client = "0.55.3" base64 = "0.21.2" dyn-clone = "1.0.11" @@ -24,6 +25,8 @@ once_cell = "1.18.0" serde = { version = "1.0.163", features = ["derive"] } thiserror = "1.0.40" tokio = "1.28.2" +hyper-proxy = "0.9.1" +hyper = "0.14.26" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } diff --git a/crates/external_services/src/email.rs b/crates/external_services/src/email.rs index b2bf99d8e01d..1d389f58298a 100644 --- a/crates/external_services/src/email.rs +++ b/crates/external_services/src/email.rs @@ -1,127 +1,163 @@ //! Interactions with the AWS SES SDK -use aws_config::meta::region::RegionProviderChain; -use aws_sdk_sesv2::{ - config::Region, - operation::send_email::SendEmailError, - types::{Body, Content, Destination, EmailContent, Message}, - Client, -}; +use aws_sdk_sesv2::types::Body; use common_utils::{errors::CustomResult, pii}; -use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; use serde::Deserialize; +/// Implementation of aws ses client +pub mod ses; + /// Custom Result type alias for Email operations. pub type EmailResult = CustomResult; /// A trait that defines the methods that must be implemented to send email. #[async_trait::async_trait] pub trait EmailClient: Sync + Send + dyn_clone::DynClone { + /// The rich text type of the email client + type RichText; + /// Sends an email to the specified recipient with the given subject and body. async fn send_email( &self, recipient: pii::Email, subject: String, - body: String, + body: Self::RichText, + proxy_url: Option<&String>, + ) -> EmailResult<()>; + + /// Convert Stringified HTML to client native rich text format + /// This has to be done because not all clients may format html as the same + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult + where + Self::RichText: Send; +} + +/// A super trait which is automatically implemented for all EmailClients +#[async_trait::async_trait] +pub trait EmailService: Sync + Send + dyn_clone::DynClone { + /// Compose and send email using the email data + async fn compose_and_send_email( + &self, + email_data: Box, + proxy_url: Option<&String>, ) -> EmailResult<()>; } -dyn_clone::clone_trait_object!(EmailClient); +#[async_trait::async_trait] +impl EmailService for T +where + T: EmailClient, + ::RichText: Send, +{ + async fn compose_and_send_email( + &self, + email_data: Box, + proxy_url: Option<&String>, + ) -> EmailResult<()> { + let email_data = email_data.get_email_data(); + let email_data = email_data.await?; + + let EmailContents { + subject, + body, + recipient, + } = email_data; + + let rich_text_string = self.convert_to_rich_text(body)?; + + self.send_email(recipient, subject, rich_text_string, proxy_url) + .await + } +} + +/// This is a struct used to create Intermediate String for rich text ( html ) +#[derive(Debug)] +pub struct IntermediateString(String); + +impl IntermediateString { + /// Create a new Instance of IntermediateString using a string + pub fn new(inner: String) -> Self { + Self(inner) + } + + /// Get the inner String + pub fn into_inner(self) -> String { + self.0 + } +} + +/// Temporary output for the email subject +#[derive(Debug)] +pub struct EmailContents { + /// The subject of email + pub subject: String, + + /// This will be the intermediate representation of the the email body in a generic format. + /// The email clients can convert this intermediate representation to their client specific rich text format + pub body: IntermediateString, + + /// The email of the recipient to whom the email has to be sent + pub recipient: pii::Email, +} + +/// A trait which will contain the logic of generating the email subject and body +#[async_trait::async_trait] +pub trait EmailData { + /// Get the email contents + async fn get_email_data(&self) -> CustomResult; +} + +dyn_clone::clone_trait_object!(EmailClient); + +/// List of available email clients to choose from +#[derive(Debug, Clone, Default, Deserialize)] +pub enum AvailableEmailClients { + #[default] + /// AWS ses email client + SES, +} /// Struct that contains the settings required to construct an EmailClient. #[derive(Debug, Clone, Default, Deserialize)] pub struct EmailSettings { - /// Sender email. - pub from_email: String, - /// The AWS region to send SES requests to. pub aws_region: String, /// Base-url used when adding links that should redirect to self pub base_url: String, -} -/// Client for AWS SES operation -#[derive(Debug, Clone)] -pub struct AwsSes { - ses_client: Client, - from_email: String, -} + /// Number of days for verification of the email + pub allowed_unverified_days: i64, -impl AwsSes { - /// Constructs a new AwsSes client - pub async fn new(conf: &EmailSettings) -> Self { - let region_provider = RegionProviderChain::first_try(Region::new(conf.aws_region.clone())); - let sdk_config = aws_config::from_env().region(region_provider).load().await; + /// Sender email + pub sender_email: String, - Self { - ses_client: Client::new(&sdk_config), - from_email: conf.from_email.clone(), - } - } -} + /// Configs related to AWS Simple Email Service + pub aws_ses: Option, -#[async_trait::async_trait] -impl EmailClient for AwsSes { - async fn send_email( - &self, - recipient: pii::Email, - subject: String, - body: String, - ) -> EmailResult<()> { - self.ses_client - .send_email() - .from_email_address(self.from_email.to_owned()) - .destination( - Destination::builder() - .to_addresses(recipient.peek()) - .build(), - ) - .content( - EmailContent::builder() - .simple( - Message::builder() - .subject(Content::builder().data(subject).build()) - .body( - Body::builder() - .text(Content::builder().data(body).charset("UTF-8").build()) - .build(), - ) - .build(), - ) - .build(), - ) - .send() - .await - .map_err(AwsSesError::SendingFailure) - .into_report() - .change_context(EmailError::EmailSendingFailure)?; - - Ok(()) - } + /// The active email client to use + pub active_email_client: AvailableEmailClients, } -#[allow(missing_docs)] /// Errors that could occur from EmailClient. #[derive(Debug, thiserror::Error)] pub enum EmailError { /// An error occurred when building email client. #[error("Error building email client")] ClientBuildingFailure, + /// An error occurred when sending email #[error("Error sending email to recipient")] EmailSendingFailure, + + /// Failed to generate the email token #[error("Failed to generate email token")] TokenGenerationFailure, + + /// The expected feature is not implemented #[error("Feature not implemented")] NotImplemented, } - -/// Errors that could occur during SES operations. -#[derive(Debug, thiserror::Error)] -pub enum AwsSesError { - /// An error occurred in the SDK while sending email. - #[error("Failed to Send Email {0:?}")] - SendingFailure(aws_smithy_client::SdkError), -} diff --git a/crates/external_services/src/email/ses.rs b/crates/external_services/src/email/ses.rs new file mode 100644 index 000000000000..7e521a5bc1c4 --- /dev/null +++ b/crates/external_services/src/email/ses.rs @@ -0,0 +1,257 @@ +use std::time::{Duration, SystemTime}; + +use aws_sdk_sesv2::{ + config::Region, + operation::send_email::SendEmailError, + types::{Body, Content, Destination, EmailContent, Message}, + Client, +}; +use aws_sdk_sts::config::Credentials; +use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii}; +use error_stack::{report, IntoReport, ResultExt}; +use hyper::Uri; +use masking::PeekInterface; +use router_env::logger; +use tokio::sync::OnceCell; + +use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString}; + +/// Client for AWS SES operation +#[derive(Debug, Clone)] +pub struct AwsSes { + ses_client: OnceCell, + sender: String, + settings: EmailSettings, +} + +/// Struct that contains the AWS ses specific configs required to construct an SES email client +#[derive(Debug, Clone, Default, serde::Deserialize)] +pub struct SESConfig { + /// The arn of email role + pub email_role_arn: String, + + /// The name of sts_session role + pub sts_role_session_name: String, +} + +/// Errors that could occur during SES operations. +#[derive(Debug, thiserror::Error)] +pub enum AwsSesError { + /// An error occurred in the SDK while sending email. + #[error("Failed to Send Email {0:?}")] + SendingFailure(aws_smithy_client::SdkError), + + /// Configuration variable is missing to construct the email client + #[error("Missing configuration variable {0}")] + MissingConfigurationVariable(&'static str), + + /// Failed to assume the given STS role + #[error("Failed to STS assume role: Role ARN: {role_arn}, Session name: {session_name}, Region: {region}")] + AssumeRoleFailure { + /// Aws region + region: String, + + /// arn of email role + role_arn: String, + + /// The name of sts_session role + session_name: String, + }, + + /// Temporary credentials are missing + #[error("Assumed role does not contain credentials for role user: {0:?}")] + TemporaryCredentialsMissing(String), + + /// The proxy Connector cannot be built + #[error("The proxy build cannot be built")] + BuildingProxyConnectorFailed, +} + +impl AwsSes { + /// Constructs a new AwsSes client + pub async fn create(conf: &EmailSettings, proxy_url: Option>) -> Self { + Self { + ses_client: OnceCell::new_with( + Self::create_client(conf, proxy_url) + .await + .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) + .ok(), + ), + sender: conf.sender_email.clone(), + settings: conf.clone(), + } + } + + /// A helper function to create ses client + pub async fn create_client( + conf: &EmailSettings, + proxy_url: Option>, + ) -> CustomResult { + let sts_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url.as_ref())? + .load() + .await; + + let ses_config = conf + .aws_ses + .as_ref() + .get_required_value("aws ses configuration") + .attach_printable("The selected email client is aws ses, but configuration is missing") + .change_context(AwsSesError::MissingConfigurationVariable("aws_ses"))?; + + let role = aws_sdk_sts::Client::new(&sts_config) + .assume_role() + .role_arn(&ses_config.email_role_arn) + .role_session_name(&ses_config.sts_role_session_name) + .send() + .await + .into_report() + .change_context(AwsSesError::AssumeRoleFailure { + region: conf.aws_region.to_owned(), + role_arn: ses_config.email_role_arn.to_owned(), + session_name: ses_config.sts_role_session_name.to_owned(), + })?; + + let creds = role.credentials().ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Credentials object not available"), + )?; + + let credentials = Credentials::new( + creds + .access_key_id() + .ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Access Key ID not found"), + )? + .to_owned(), + creds + .secret_access_key() + .ok_or( + report!(AwsSesError::TemporaryCredentialsMissing(format!( + "{role:?}" + ))) + .attach_printable("Secret Access Key not found"), + )? + .to_owned(), + creds.session_token().map(|s| s.to_owned()), + creds.expiration().and_then(|dt| { + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_nanos(u64::try_from(dt.as_nanos()).ok()?)) + }), + "custom_provider", + ); + + logger::debug!( + "Obtained SES temporary credentials with expiry {:?}", + credentials.expiry() + ); + + let ses_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url)? + .credentials_provider(credentials) + .load() + .await; + + Ok(Client::new(&ses_config)) + } + + fn get_shared_config( + region: String, + proxy_url: Option>, + ) -> CustomResult { + let region_provider = Region::new(region); + let mut config = aws_config::from_env().region(region_provider); + if let Some(proxy_url) = proxy_url { + let proxy_connector = Self::get_proxy_connector(proxy_url)?; + let provider_config = aws_config::provider_config::ProviderConfig::default() + .with_tcp_connector(proxy_connector.clone()); + let http_connector = + aws_smithy_client::hyper_ext::Adapter::builder().build(proxy_connector); + config = config + .configure(provider_config) + .http_connector(http_connector); + }; + Ok(config) + } + + fn get_proxy_connector( + proxy_url: impl AsRef, + ) -> CustomResult, AwsSesError> { + let proxy_uri = proxy_url + .as_ref() + .parse::() + .into_report() + .attach_printable("Unable to parse the proxy url {proxy_url}") + .change_context(AwsSesError::BuildingProxyConnectorFailed)?; + + let proxy = hyper_proxy::Proxy::new(hyper_proxy::Intercept::All, proxy_uri); + + hyper_proxy::ProxyConnector::from_proxy(hyper::client::HttpConnector::new(), proxy) + .into_report() + .change_context(AwsSesError::BuildingProxyConnectorFailed) + } +} + +#[async_trait::async_trait] +impl EmailClient for AwsSes { + type RichText = Body; + + fn convert_to_rich_text( + &self, + intermediate_string: IntermediateString, + ) -> CustomResult { + let email_body = Body::builder() + .html( + Content::builder() + .data(intermediate_string.into_inner()) + .charset("UTF-8") + .build(), + ) + .build(); + + Ok(email_body) + } + + async fn send_email( + &self, + recipient: pii::Email, + subject: String, + body: Self::RichText, + proxy_url: Option<&String>, + ) -> EmailResult<()> { + self.ses_client + .get_or_try_init(|| async { + Self::create_client(&self.settings, proxy_url) + .await + .change_context(EmailError::ClientBuildingFailure) + }) + .await? + .send_email() + .from_email_address(self.sender.to_owned()) + .destination( + Destination::builder() + .to_addresses(recipient.peek()) + .build(), + ) + .content( + EmailContent::builder() + .simple( + Message::builder() + .subject(Content::builder().data(subject).build()) + .body(body) + .build(), + ) + .build(), + ) + .send() + .await + .map_err(AwsSesError::SendingFailure) + .into_report() + .change_context(EmailError::EmailSendingFailure)?; + + Ok(()) + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index a5f8b2f6b847..b51dc045b20d 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -12,7 +12,7 @@ license.workspace = true default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] -email = ["external_services/email", "dep:aws-config"] +email = ["external_services/email", "dep:aws-config", "olap"] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 4f19562c83ce..61072d06221b 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -62,4 +62,6 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes #[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days +#[cfg(feature = "email")] +pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 94cd482a2291..1dc0e2e1a112 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -70,6 +70,28 @@ pub async fn connect_account( .get_jwt_auth_token(state.clone(), user_role.org_id) .await?; + #[cfg(feature = "email")] + { + use router_env::logger; + + use crate::services::email::types as email_types; + + let email_contents = email_types::WelcomeEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + } + return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { token: Secret::new(jwt_token), merchant_id: user_role.merchant_id, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ae0e0f04f598..1a6f36363d1d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use actix_web::{web, Scope}; #[cfg(feature = "email")] -use external_services::email::{AwsSes, EmailClient}; +use external_services::email::{ses::AwsSes, EmailService}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; use router_env::tracing_actix_web::RequestId; @@ -45,7 +45,7 @@ pub struct AppState { pub conf: Arc, pub event_handler: Box, #[cfg(feature = "email")] - pub email_client: Arc, + pub email_client: Arc, #[cfg(feature = "kms")] pub kms_secrets: Arc, pub api_client: Box, @@ -64,7 +64,7 @@ pub trait AppStateInfo { fn store(&self) -> Box; fn event_handler(&self) -> Box; #[cfg(feature = "email")] - fn email_client(&self) -> Arc; + fn email_client(&self) -> Arc; fn add_request_id(&mut self, request_id: RequestId); fn add_merchant_id(&mut self, merchant_id: Option); fn add_flow_name(&mut self, flow_name: String); @@ -79,7 +79,7 @@ impl AppStateInfo for AppState { self.store.to_owned() } #[cfg(feature = "email")] - fn email_client(&self) -> Arc { + fn email_client(&self) -> Arc { self.email_client.to_owned() } fn event_handler(&self) -> Box { @@ -107,6 +107,15 @@ impl AsRef for AppState { } } +#[cfg(feature = "email")] +pub async fn create_email_client(settings: &settings::Settings) -> impl EmailService { + match settings.email.active_email_client { + external_services::email::AvailableEmailClients::SES => { + AwsSes::create(&settings.email, settings.proxy.https_url.to_owned()).await + } + } +} + impl AppState { /// # Panics /// @@ -154,7 +163,8 @@ impl AppState { .expect("Failed while performing KMS decryption"); #[cfg(feature = "email")] - let email_client = Arc::new(AwsSes::new(&conf.email).await); + let email_client = Arc::new(create_email_client(&conf).await); + Self { flow_name: String::from("default"), store, diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 2d5552b59d17..faea707f2a14 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -6,6 +6,9 @@ pub mod encryption; pub mod jwt; pub mod logger; +#[cfg(feature = "email")] +pub mod email; + #[cfg(feature = "kms")] use data_models::errors::StorageError; use data_models::errors::StorageResult; diff --git a/crates/router/src/services/email.rs b/crates/router/src/services/email.rs new file mode 100644 index 000000000000..cd408564ea08 --- /dev/null +++ b/crates/router/src/services/email.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/crates/router/src/services/email/assets/invite.html b/crates/router/src/services/email/assets/invite.html new file mode 100644 index 000000000000..307ec6cead85 --- /dev/null +++ b/crates/router/src/services/email/assets/invite.html @@ -0,0 +1,243 @@ + +Welcome to HyperSwitch! + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Welcome to HyperSwitch! +
+
+ Hi {username}
+
+
+ You have received this email because your administrator has invited you as a new user on + Hyperswitch. +
+
+
+ To get started, click on the button below. +
+ + + + +
+ Click here to Join +
+
+
+ If the link has already expired, you can request a new link from your administrator or reach out to + your internal support for more assistance.
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/magic_link.html b/crates/router/src/services/email/assets/magic_link.html new file mode 100644 index 000000000000..6439c83f227c --- /dev/null +++ b/crates/router/src/services/email/assets/magic_link.html @@ -0,0 +1,260 @@ + +Login to Hyperswitch + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Welcome to Hyperswitch! +

Dear {user_name},

+ We are thrilled to welcome you into our community! + +
+
+ Simply click on the link below, and you'll be granted instant access + to your Hyperswitch account. Note that this link expires in 24 hours + and can only be used once.
+
+ + + + +
+ Unlock Hyperswitch +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/recon_activated.html b/crates/router/src/services/email/assets/recon_activated.html new file mode 100644 index 000000000000..7feffacb09df --- /dev/null +++ b/crates/router/src/services/email/assets/recon_activated.html @@ -0,0 +1,309 @@ + +Access Granted to HyperSwitch Recon Dashboard! + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Access Granted to HyperSwitch Recon Dashboard! +
+
+ Dear {username}
+
+
+ We are pleased to inform you that your Reconciliation access request + has been approved. As a result, you now have authorized access to the + Recon dashboard, allowing you to test its functionality and experience + its benefits firsthand. +
+
+
+ To access the Recon dashboard, please follow these steps +
+
+
    +
  1. + Visit our website at + Hyperswitch Dashboard. +
  2. +
  3. Click on the "Login" button.
  4. +
  5. Enter your login credentials to log in.
  6. +
  7. + Once logged in, you will have full access to the Recon dashboard, + where you can explore its comprehensive features. +
  8. +
+ Should you have any inquiries or require any form of assistance, + please do not hesitate to reach out to our team on + Slack , + and we will be more than willing to assist you promptly.

+ Wishing you a seamless and successful experience as you explore the + capabilities of Hyperswitch.
+
+ Thanks,
+ Team Hyperswitch +
+
+ \ No newline at end of file diff --git a/crates/router/src/services/email/assets/reset.html b/crates/router/src/services/email/assets/reset.html new file mode 100644 index 000000000000..98ddf8a7bd16 --- /dev/null +++ b/crates/router/src/services/email/assets/reset.html @@ -0,0 +1,229 @@ + +Hyperswitch Merchant + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Reset Your Password +
+
+ Hey {username}
+
+
+ We have received a request to reset your password associated with +
+ username : + {username}
+
+
+ Click on the below button to reset your password.
+
+ + + + +
+ Reset Password +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/assets/verify.html b/crates/router/src/services/email/assets/verify.html new file mode 100644 index 000000000000..47d0e3b5c6d5 --- /dev/null +++ b/crates/router/src/services/email/assets/verify.html @@ -0,0 +1,253 @@ + +Hyperswitch Merchant + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Thanks for signing up!
We need a confirmation of your email address to complete your + registration. +
+
+ Click below to confirm your email address.
+
+ + + + +
+ Verify Email Now +
+
+ Thanks,
+ Team Hyperswitch +
+
+ diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs new file mode 100644 index 000000000000..8650e1c27c22 --- /dev/null +++ b/crates/router/src/services/email/types.rs @@ -0,0 +1,80 @@ +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use external_services::email::{EmailContents, EmailData, EmailError}; +use masking::ExposeInterface; + +use crate::{configs, consts}; +#[cfg(feature = "olap")] +use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail}; + +pub enum EmailBody { + Verify { link: String }, +} + +pub mod html { + use crate::services::email::types::EmailBody; + + pub fn get_html_body(email_body: EmailBody) -> String { + match email_body { + EmailBody::Verify { link } => { + format!(include_str!("assets/verify.html"), link = link) + } + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct EmailToken { + email: String, + expiration: u64, +} + +impl EmailToken { + pub async fn new_token( + email: UserEmail, + settings: &configs::settings::Settings, + ) -> CustomResult { + let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); + let expiration = jwt::generate_exp(expiration_duration)?.as_secs(); + let token_payload = Self { + email: email.get_secret().expose(), + expiration, + }; + jwt::generate_jwt(&token_payload, settings).await + } +} + +pub struct WelcomeEmail { + pub recipient_email: UserEmail, + pub settings: std::sync::Arc, +} + +pub fn get_email_verification_link( + base_url: impl std::fmt::Display, + token: impl std::fmt::Display, +) -> String { + format!("{base_url}/user/verify_email/?token={token}") +} + +/// Currently only HTML is supported +#[async_trait::async_trait] +impl EmailData for WelcomeEmail { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token); + + let body = html::get_html_body(EmailBody::Verify { + link: verify_email_link, + }); + let subject = "Welcome to the Hyperswitch community!".to_string(); + + Ok(EmailContents { + subject, + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} From 2e57745352c547323ac2df2554f6bc2dbd6da37f Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:52:35 +0530 Subject: [PATCH 101/443] fix(router): make use of warning to log errors when apple pay metadata parsing fails (#3010) --- crates/router/src/core/payments.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index db83dce487a6..33afa29397e1 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1554,7 +1554,7 @@ fn check_apple_pay_metadata( }) }) .map_err( - |error| logger::error!(%error, "Failed to Parse Value to ApplepaySessionTokenData"), + |error| logger::warn!(%error, "Failed to Parse Value to ApplepaySessionTokenData"), ); parsed_metadata.ok().map(|metadata| match metadata { From 9df4e0193ffeb6d1cc323bdebb7e2bdfb2a375e2 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Wed, 29 Nov 2023 17:04:53 +0530 Subject: [PATCH 102/443] feat(analytics): Add Clickhouse based analytics (#2988) Co-authored-by: harsh_sharma_juspay Co-authored-by: Ivor Dsouza Co-authored-by: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Co-authored-by: nain-F49FF806 <126972030+nain-F49FF806@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: akshay.s Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- Cargo.lock | 121 +- Dockerfile | 4 +- config/development.toml | 30 + config/docker_compose.toml | 18 +- crates/analytics/Cargo.toml | 37 + crates/analytics/docs/clickhouse/README.md | 45 + .../docs/clickhouse/cluster_setup/README.md | 347 +++ .../config/clickhouse_config.xml | 370 ++++ .../config/clickhouse_metrika.xml | 60 + .../cluster_setup/config/macros/macros-01.xml | 9 + .../cluster_setup/config/macros/macros-02.xml | 9 + .../cluster_setup/config/macros/macros-03.xml | 9 + .../cluster_setup/config/macros/macros-04.xml | 9 + .../cluster_setup/config/macros/macros-05.xml | 9 + .../cluster_setup/config/macros/macros-06.xml | 9 + .../clickhouse/cluster_setup/config/users.xml | 117 + .../cluster_setup/docker-compose.yml | 198 ++ .../clickhouse/cluster_setup/kafka-script.sh | 11 + .../cluster_setup/scripts/api_event_logs.sql | 237 ++ .../scripts/payment_attempts.sql | 217 ++ .../cluster_setup/scripts/payment_intents.sql | 165 ++ .../scripts/refund_analytics.sql | 173 ++ .../cluster_setup/scripts/sdk_events.sql | 156 ++ .../cluster_setup/scripts/seed_scripts.sql | 1 + .../docs/clickhouse/scripts/api_events_v2.sql | 134 ++ .../clickhouse/scripts/payment_attempts.sql | 156 ++ .../clickhouse/scripts/payment_intents.sql | 116 + .../docs/clickhouse/scripts/refunds.sql | 121 ++ crates/analytics/src/api_event.rs | 9 + crates/analytics/src/api_event/core.rs | 176 ++ crates/analytics/src/api_event/events.rs | 105 + crates/analytics/src/api_event/filters.rs | 53 + crates/analytics/src/api_event/metrics.rs | 110 + .../src/api_event/metrics/api_count.rs | 106 + .../src/api_event/metrics/latency.rs | 138 ++ .../api_event/metrics/status_code_count.rs | 103 + crates/analytics/src/api_event/types.rs | 33 + crates/analytics/src/clickhouse.rs | 458 ++++ crates/analytics/src/core.rs | 31 + .../src/analytics => analytics/src}/errors.rs | 0 crates/analytics/src/lambda_utils.rs | 36 + crates/analytics/src/lib.rs | 509 +++++ crates/analytics/src/main.rs | 3 + .../analytics => analytics/src}/metrics.rs | 0 .../src}/metrics/request.rs | 28 +- crates/analytics/src/payments.rs | 16 + .../src}/payments/accumulator.rs | 72 +- crates/analytics/src/payments/core.rs | 303 +++ crates/analytics/src/payments/distribution.rs | 92 + .../distribution/payment_error_message.rs | 176 ++ .../src}/payments/filters.rs | 10 +- .../src}/payments/metrics.rs | 41 +- .../src}/payments/metrics/avg_ticket_size.rs | 16 +- .../metrics/connector_success_rate.rs | 130 ++ .../src}/payments/metrics/payment_count.rs | 8 +- .../metrics/payment_processed_amount.rs | 10 +- .../payments/metrics/payment_success_count.rs | 10 +- .../src/payments/metrics/retries_count.rs | 122 ++ .../src}/payments/metrics/success_rate.rs | 8 +- .../src}/payments/types.rs | 11 +- .../src/analytics => analytics/src}/query.rs | 272 ++- .../analytics => analytics/src}/refunds.rs | 2 +- .../src}/refunds/accumulator.rs | 4 +- crates/analytics/src/refunds/core.rs | 203 ++ .../src}/refunds/filters.rs | 10 +- .../src}/refunds/metrics.rs | 13 +- .../src}/refunds/metrics/refund_count.rs | 9 +- .../metrics/refund_processed_amount.rs | 9 +- .../refunds/metrics/refund_success_count.rs | 9 +- .../refunds/metrics/refund_success_rate.rs | 7 +- .../src}/refunds/types.rs | 2 +- crates/analytics/src/sdk_events.rs | 14 + .../analytics/src/sdk_events/accumulator.rs | 98 + crates/analytics/src/sdk_events/core.rs | 201 ++ crates/analytics/src/sdk_events/events.rs | 80 + crates/analytics/src/sdk_events/filters.rs | 56 + crates/analytics/src/sdk_events/metrics.rs | 181 ++ .../metrics/average_payment_time.rs | 129 ++ .../sdk_events/metrics/payment_attempts.rs | 118 + .../metrics/payment_data_filled_count.rs | 118 + .../metrics/payment_method_selected_count.rs | 118 + .../metrics/payment_methods_call_count.rs | 126 ++ .../metrics/payment_success_count.rs | 118 + .../sdk_events/metrics/sdk_initiated_count.rs | 118 + .../sdk_events/metrics/sdk_rendered_count.rs | 118 + crates/analytics/src/sdk_events/types.rs | 50 + .../src/analytics => analytics/src}/sqlx.rs | 189 +- .../src/analytics => analytics/src}/types.rs | 26 +- .../src/analytics => analytics/src}/utils.rs | 18 + crates/api_models/src/analytics.rs | 154 +- crates/api_models/src/analytics/api_event.rs | 148 ++ crates/api_models/src/analytics/payments.rs | 52 +- crates/api_models/src/analytics/refunds.rs | 21 +- crates/api_models/src/analytics/sdk_events.rs | 215 ++ crates/api_models/src/events.rs | 36 +- crates/api_models/src/payments.rs | 2 + crates/data_models/Cargo.toml | 1 - crates/router/Cargo.toml | 4 +- crates/router/src/analytics.rs | 655 +++++- crates/router/src/analytics/core.rs | 96 - crates/router/src/analytics/payments.rs | 13 - crates/router/src/analytics/payments/core.rs | 129 -- crates/router/src/analytics/refunds/core.rs | 104 - crates/router/src/analytics/routes.rs | 164 -- crates/router/src/bin/scheduler.rs | 2 - crates/router/src/configs/kms.rs | 2 +- crates/router/src/configs/settings.rs | 25 +- crates/router/src/core/refunds.rs | 4 +- crates/router/src/core/webhooks.rs | 2 + crates/router/src/db.rs | 94 +- crates/router/src/db/kafka_store.rs | 1917 +++++++++++++++++ crates/router/src/events.rs | 64 +- crates/router/src/events/api_logs.rs | 6 + crates/router/src/events/event_logger.rs | 2 +- crates/router/src/events/kafka_handler.rs | 29 + crates/router/src/lib.rs | 9 +- crates/router/src/routes/app.rs | 49 +- crates/router/src/services.rs | 1 + crates/router/src/services/api.rs | 2 + crates/router/src/services/kafka.rs | 314 +++ crates/router/src/services/kafka/api_event.rs | 108 + .../src/services/kafka/outgoing_request.rs | 19 + .../src/services/kafka/payment_attempt.rs | 92 + .../src/services/kafka/payment_intent.rs | 71 + crates/router/src/services/kafka/refund.rs | 68 + .../src/types/storage/payment_attempt.rs | 4 - crates/router/tests/connectors/aci.rs | 4 + crates/router/tests/connectors/utils.rs | 9 + crates/router/tests/payments2.rs | 1 + crates/router/tests/utils.rs | 1 + crates/router_env/src/lib.rs | 13 +- crates/scheduler/Cargo.toml | 2 +- crates/storage_impl/src/config.rs | 38 +- crates/storage_impl/src/database/store.rs | 2 +- docker-compose.yml | 63 + 135 files changed, 12141 insertions(+), 897 deletions(-) create mode 100644 crates/analytics/Cargo.toml create mode 100644 crates/analytics/docs/clickhouse/README.md create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/README.md create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/config/users.xml create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml create mode 100755 crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql create mode 100644 crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/api_events_v2.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/payment_attempts.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/payment_intents.sql create mode 100644 crates/analytics/docs/clickhouse/scripts/refunds.sql create mode 100644 crates/analytics/src/api_event.rs create mode 100644 crates/analytics/src/api_event/core.rs create mode 100644 crates/analytics/src/api_event/events.rs create mode 100644 crates/analytics/src/api_event/filters.rs create mode 100644 crates/analytics/src/api_event/metrics.rs create mode 100644 crates/analytics/src/api_event/metrics/api_count.rs create mode 100644 crates/analytics/src/api_event/metrics/latency.rs create mode 100644 crates/analytics/src/api_event/metrics/status_code_count.rs create mode 100644 crates/analytics/src/api_event/types.rs create mode 100644 crates/analytics/src/clickhouse.rs create mode 100644 crates/analytics/src/core.rs rename crates/{router/src/analytics => analytics/src}/errors.rs (100%) create mode 100644 crates/analytics/src/lambda_utils.rs create mode 100644 crates/analytics/src/lib.rs create mode 100644 crates/analytics/src/main.rs rename crates/{router/src/analytics => analytics/src}/metrics.rs (100%) rename crates/{router/src/analytics => analytics/src}/metrics/request.rs (51%) create mode 100644 crates/analytics/src/payments.rs rename crates/{router/src/analytics => analytics/src}/payments/accumulator.rs (62%) create mode 100644 crates/analytics/src/payments/core.rs create mode 100644 crates/analytics/src/payments/distribution.rs create mode 100644 crates/analytics/src/payments/distribution/payment_error_message.rs rename crates/{router/src/analytics => analytics/src}/payments/filters.rs (87%) rename crates/{router/src/analytics => analytics/src}/payments/metrics.rs (76%) rename crates/{router/src/analytics => analytics/src}/payments/metrics/avg_ticket_size.rs (90%) create mode 100644 crates/analytics/src/payments/metrics/connector_success_rate.rs rename crates/{router/src/analytics => analytics/src}/payments/metrics/payment_count.rs (94%) rename crates/{router/src/analytics => analytics/src}/payments/metrics/payment_processed_amount.rs (94%) rename crates/{router/src/analytics => analytics/src}/payments/metrics/payment_success_count.rs (94%) create mode 100644 crates/analytics/src/payments/metrics/retries_count.rs rename crates/{router/src/analytics => analytics/src}/payments/metrics/success_rate.rs (95%) rename crates/{router/src/analytics => analytics/src}/payments/types.rs (82%) rename crates/{router/src/analytics => analytics/src}/query.rs (65%) rename crates/{router/src/analytics => analytics/src}/refunds.rs (81%) rename crates/{router/src/analytics => analytics/src}/refunds/accumulator.rs (98%) create mode 100644 crates/analytics/src/refunds/core.rs rename crates/{router/src/analytics => analytics/src}/refunds/filters.rs (90%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics.rs (91%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_count.rs (94%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_processed_amount.rs (95%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_success_count.rs (95%) rename crates/{router/src/analytics => analytics/src}/refunds/metrics/refund_success_rate.rs (96%) rename crates/{router/src/analytics => analytics/src}/refunds/types.rs (98%) create mode 100644 crates/analytics/src/sdk_events.rs create mode 100644 crates/analytics/src/sdk_events/accumulator.rs create mode 100644 crates/analytics/src/sdk_events/core.rs create mode 100644 crates/analytics/src/sdk_events/events.rs create mode 100644 crates/analytics/src/sdk_events/filters.rs create mode 100644 crates/analytics/src/sdk_events/metrics.rs create mode 100644 crates/analytics/src/sdk_events/metrics/average_payment_time.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_attempts.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/payment_success_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs create mode 100644 crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs create mode 100644 crates/analytics/src/sdk_events/types.rs rename crates/{router/src/analytics => analytics/src}/sqlx.rs (64%) rename crates/{router/src/analytics => analytics/src}/types.rs (83%) rename crates/{router/src/analytics => analytics/src}/utils.rs (52%) create mode 100644 crates/api_models/src/analytics/api_event.rs create mode 100644 crates/api_models/src/analytics/sdk_events.rs delete mode 100644 crates/router/src/analytics/core.rs delete mode 100644 crates/router/src/analytics/payments.rs delete mode 100644 crates/router/src/analytics/payments/core.rs delete mode 100644 crates/router/src/analytics/refunds/core.rs delete mode 100644 crates/router/src/analytics/routes.rs create mode 100644 crates/router/src/db/kafka_store.rs create mode 100644 crates/router/src/events/kafka_handler.rs create mode 100644 crates/router/src/services/kafka.rs create mode 100644 crates/router/src/services/kafka/api_event.rs create mode 100644 crates/router/src/services/kafka/outgoing_request.rs create mode 100644 crates/router/src/services/kafka/payment_attempt.rs create mode 100644 crates/router/src/services/kafka/payment_intent.rs create mode 100644 crates/router/src/services/kafka/refund.rs diff --git a/Cargo.lock b/Cargo.lock index 96bdcff3f86e..417e6d85db6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,36 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "analytics" +version = "0.1.0" +dependencies = [ + "actix-web", + "api_models", + "async-trait", + "aws-config", + "aws-sdk-lambda", + "aws-smithy-types", + "bigdecimal", + "common_utils", + "diesel_models", + "error-stack", + "external_services", + "futures 0.3.28", + "masking", + "once_cell", + "reqwest", + "router_env", + "serde", + "serde_json", + "sqlx", + "storage_impl", + "strum 0.25.0", + "thiserror", + "time", + "tokio 1.32.0", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -729,6 +759,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-lambda" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ad176ffaa3aafa532246eb6a9f18a7d68da19950704ecc95d33d9dc3c62a9b" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-types", + "bytes 1.5.0", + "http", + "regex", + "tokio-stream", + "tower", + "tracing", +] + [[package]] name = "aws-sdk-s3" version = "0.28.0" @@ -1148,6 +1203,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] @@ -1256,7 +1312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f404657a7ea7b5249e36808dff544bc88a28f26e0ac40009f674b7a009d14be3" dependencies = [ "once_cell", - "proc-macro-crate", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 2.0.38", @@ -3862,6 +3918,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "object" version = "0.32.1" @@ -4395,6 +4472,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.10", +] + [[package]] name = "proc-macro-crate" version = "2.0.0" @@ -4688,6 +4775,36 @@ dependencies = [ "crossbeam-utils 0.8.16", ] +[[package]] +name = "rdkafka" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54f02a5a40220f8a2dfa47ddb38ba9064475a5807a69504b6f91711df2eea63" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio 1.32.0", +] + +[[package]] +name = "rdkafka-sys" +version = "4.7.0+2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" +dependencies = [ + "libc", + "libz-sys", + "num_enum", + "pkg-config", +] + [[package]] name = "redis-protocol" version = "4.1.0" @@ -4939,6 +5056,7 @@ dependencies = [ "actix-multipart", "actix-rt", "actix-web", + "analytics", "api_models", "argon2", "async-bb8-diesel", @@ -4988,6 +5106,7 @@ dependencies = [ "qrcode", "rand 0.8.5", "rand_chacha 0.3.1", + "rdkafka", "redis_interface", "regex", "reqwest", diff --git a/Dockerfile b/Dockerfile index 8eb321dd2afd..e9591e5e9f27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:slim-bookworm as builder +FROM rust:bookworm as builder ARG EXTRA_FEATURES="" @@ -36,7 +36,7 @@ RUN cargo build --release --features release ${EXTRA_FEATURES} -FROM debian:bookworm-slim +FROM debian:bookworm # Placing config and binary executable in different directories ARG CONFIG_DIR=/local/config diff --git a/config/development.toml b/config/development.toml index f2620bd37135..fa5fddb0d60a 100644 --- a/config/development.toml +++ b/config/development.toml @@ -475,3 +475,33 @@ delay_between_retries_in_milliseconds = 500 [kv_config] ttl = 900 # 15 * 60 seconds + +[events] +source = "logs" + +[events.kafka] +brokers = ["localhost:9092"] +intent_analytics_topic = "hyperswitch-payment-intent-events" +attempt_analytics_topic = "hyperswitch-payment-attempt-events" +refund_analytics_topic = "hyperswitch-refund-events" +api_logs_topic = "hyperswitch-api-log-events" +connector_events_topic = "hyperswitch-connector-api-events" + +[analytics] +source = "sqlx" + +[analytics.clickhouse] +username = "default" +# password = "" +host = "http://localhost:8123" +database_name = "default" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "localhost" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 +connection_timeout = 10 +queue_strategy = "Fifo" \ No newline at end of file diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 445e1e856846..4d50600e1bf8 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -333,16 +333,32 @@ supported_connectors = "braintree" redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 +[events.kafka] +brokers = ["localhost:9092"] +intent_analytics_topic = "hyperswitch-payment-intent-events" +attempt_analytics_topic = "hyperswitch-payment-attempt-events" +refund_analytics_topic = "hyperswitch-refund-events" +api_logs_topic = "hyperswitch-api-log-events" +connector_events_topic = "hyperswitch-connector-api-events" + [analytics] source = "sqlx" +[analytics.clickhouse] +username = "default" +# password = "" +host = "http://localhost:8123" +database_name = "default" + [analytics.sqlx] username = "db_user" password = "db_pass" -host = "pg" +host = "localhost" port = 5432 dbname = "hyperswitch_db" pool_size = 5 +connection_timeout = 10 +queue_strategy = "Fifo" [kv_config] ttl = 900 # 15 * 60 seconds diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml new file mode 100644 index 000000000000..f49fe322ae3b --- /dev/null +++ b/crates/analytics/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "analytics" +version = "0.1.0" +description = "Analytics / Reports related functionality" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +# First party crates +api_models = { version = "0.1.0", path = "../api_models" , features = ["errors"]} +storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +common_utils = { version = "0.1.0", path = "../common_utils"} +external_services = { version = "0.1.0", path = "../external_services", default-features = false} +masking = { version = "0.1.0", path = "../masking" } +router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } + +#Third Party dependencies +actix-web = "4.3.1" +async-trait = "0.1.68" +aws-config = { version = "0.55.3" } +aws-sdk-lambda = { version = "0.28.0" } +aws-smithy-types = { version = "0.55.3" } +bigdecimal = { version = "0.3.1", features = ["serde"] } +error-stack = "0.3.1" +futures = "0.3.28" +once_cell = "1.18.0" +reqwest = { version = "0.11.18", features = ["serde_json"] } +serde = { version = "1.0.163", features = ["derive", "rc"] } +serde_json = "1.0.96" +sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } +strum = { version = "0.25.0", features = ["derive"] } +thiserror = "1.0.43" +time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } +tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } diff --git a/crates/analytics/docs/clickhouse/README.md b/crates/analytics/docs/clickhouse/README.md new file mode 100644 index 000000000000..2fd48a30c29f --- /dev/null +++ b/crates/analytics/docs/clickhouse/README.md @@ -0,0 +1,45 @@ +#### Starting the containers + +In our use case we rely on kafka for ingesting events. +hence we can use docker compose to start all the components + +``` +docker compose up -d clickhouse-server kafka-ui +``` + +> kafka-ui is a visual tool for inspecting kafka on localhost:8090 + +#### Setting up Clickhouse + +Once clickhouse is up & running you need to create the required tables for it + +you can either visit the url (http://localhost:8123/play) in which the clickhouse-server is running to get a playground +Alternatively you can bash into the clickhouse container & execute commands manually +``` +# On your local terminal +docker compose exec clickhouse-server bash + +# Inside the clickhouse-server container shell +clickhouse-client --user default + +# Inside the clickhouse-client shell +SHOW TABLES; +CREATE TABLE ...... +``` + +The table creation scripts are provided [here](./scripts) + +#### Running/Debugging your application +Once setup you can run your application either via docker compose or normally via cargo run + +Remember to enable the kafka_events via development.toml/docker_compose.toml files + +Inspect the [kafka-ui](http://localhost:8090) to check the messages being inserted in queue + +If the messages/topic are available then you can run select queries on your clickhouse table to ensure data is being populated... + +If the data is not being populated in clickhouse, you can check the error logs in clickhouse server via +``` +# Inside the clickhouse-server container shell +tail -f /var/log/clickhouse-server/clickhouse-server.err.log +``` \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/README.md b/crates/analytics/docs/clickhouse/cluster_setup/README.md new file mode 100644 index 000000000000..cd5f2dfeb023 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/README.md @@ -0,0 +1,347 @@ +# Tutorial for set up clickhouse server + + +## Single server with docker + + +- Run server + +``` +docker run -d --name clickhouse-server -p 9000:9000 --ulimit nofile=262144:262144 yandex/clickhouse-server + +``` + +- Run client + +``` +docker run -it --rm --link clickhouse-server:clickhouse-server yandex/clickhouse-client --host clickhouse-server +``` + +Now you can see if it success setup or not. + + +## Setup Cluster + + +This part we will setup + +- 1 cluster, with 3 shards +- Each shard has 2 replica server +- Use ReplicatedMergeTree & Distributed table to setup our table. + + +### Cluster + +Let's see our docker-compose.yml first. + +``` +version: '3' + +services: + clickhouse-zookeeper: + image: zookeeper + ports: + - "2181:2181" + - "2182:2182" + container_name: clickhouse-zookeeper + hostname: clickhouse-zookeeper + + clickhouse-01: + image: yandex/clickhouse-server + hostname: clickhouse-01 + container_name: clickhouse-01 + ports: + - 9001:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-01:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-02: + image: yandex/clickhouse-server + hostname: clickhouse-02 + container_name: clickhouse-02 + ports: + - 9002:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-02:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-03: + image: yandex/clickhouse-server + hostname: clickhouse-03 + container_name: clickhouse-03 + ports: + - 9003:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-03:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-04: + image: yandex/clickhouse-server + hostname: clickhouse-04 + container_name: clickhouse-04 + ports: + - 9004:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-04:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-05: + image: yandex/clickhouse-server + hostname: clickhouse-05 + container_name: clickhouse-05 + ports: + - 9005:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-05:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-06: + image: yandex/clickhouse-server + hostname: clickhouse-06 + container_name: clickhouse-06 + ports: + - 9006:9000 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml + # - ./data/server-06:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" +networks: + default: + external: + name: clickhouse-net +``` + + +We have 6 clickhouse server container and one zookeeper container. + + +**To enable replication ZooKeeper is required. ClickHouse will take care of data consistency on all replicas and run restore procedure after failure automatically. It's recommended to deploy ZooKeeper cluster to separate servers.** + +**ZooKeeper is not a requirement — in some simple cases you can duplicate the data by writing it into all the replicas from your application code. This approach is not recommended — in this case ClickHouse is not able to guarantee data consistency on all replicas. This remains the responsibility of your application.** + + +Let's see config file. + +`./config/clickhouse_config.xml` is the default config file in docker, we copy it out and add this line + +``` + + /etc/clickhouse-server/metrika.xml +``` + + +So lets see `clickhouse_metrika.xml` + +``` + + + + + 1 + true + + clickhouse-01 + 9000 + + + clickhouse-06 + 9000 + + + + 1 + true + + clickhouse-02 + 9000 + + + clickhouse-03 + 9000 + + + + 1 + true + + + clickhouse-04 + 9000 + + + clickhouse-05 + 9000 + + + + + + + clickhouse-zookeeper + 2181 + + + + ::/0 + + + + 10000000000 + 0.01 + lz4 + + + +``` + +and macros.xml, each instances has there own macros settings, like server 1: + +``` + + + clickhouse-01 + 01 + 01 + + +``` + + +**Make sure your macros settings is equal to remote server settings in metrika.xml** + +So now you can start the server. + +``` +docker network create clickhouse-net +docker-compose up -d +``` + +Conn to server and see if the cluster settings fine; + +``` +docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server +``` + +```sql +clickhouse-01 :) select * from system.clusters; + +SELECT * +FROM system.clusters + +┌─cluster─────────────────────┬─shard_num─┬─shard_weight─┬─replica_num─┬─host_name─────┬─host_address─┬─port─┬─is_local─┬─user────┬─default_database─┐ +│ cluster_1 │ 1 │ 1 │ 1 │ clickhouse-01 │ 172.21.0.4 │ 9000 │ 1 │ default │ │ +│ cluster_1 │ 1 │ 1 │ 2 │ clickhouse-06 │ 172.21.0.5 │ 9000 │ 1 │ default │ │ +│ cluster_1 │ 2 │ 1 │ 1 │ clickhouse-02 │ 172.21.0.8 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 2 │ 1 │ 2 │ clickhouse-03 │ 172.21.0.6 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 3 │ 1 │ 1 │ clickhouse-04 │ 172.21.0.7 │ 9000 │ 0 │ default │ │ +│ cluster_1 │ 3 │ 1 │ 2 │ clickhouse-05 │ 172.21.0.3 │ 9000 │ 0 │ default │ │ +│ test_shard_localhost │ 1 │ 1 │ 1 │ localhost │ 127.0.0.1 │ 9000 │ 1 │ default │ │ +│ test_shard_localhost_secure │ 1 │ 1 │ 1 │ localhost │ 127.0.0.1 │ 9440 │ 0 │ default │ │ +└─────────────────────────────┴───────────┴──────────────┴─────────────┴───────────────┴──────────────┴──────┴──────────┴─────────┴──────────────────┘ +``` + +If you see this, it means cluster's settings work well(but not conn fine). + + +### Replica Table + +So now we have a cluster and replica settings. For clickhouse, we need to create ReplicatedMergeTree Table as a local table in every server. + +```sql +CREATE TABLE ttt (id Int32) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ttt', '{replica}') PARTITION BY id ORDER BY id +``` + +and Create Distributed Table conn to local table + +```sql +CREATE TABLE ttt_all as ttt ENGINE = Distributed(cluster_1, default, ttt, rand()); +``` + + +### Insert and test + +gen some data and test. + + +``` +# docker exec into client server 1 and +for ((idx=1;idx<=100;++idx)); do clickhouse-client --host clickhouse-server --query "Insert into default.ttt_all values ($idx)"; done; +``` + +For Distributed table. + +``` +select count(*) from ttt_all; +``` + +For loacl table. + +``` +select count(*) from ttt; +``` + + +## Authentication + +Please see config/users.xml + + +- Conn +```bash +docker run -it --rm --network="clickhouse-net" --link clickhouse-01:clickhouse-server yandex/clickhouse-client --host clickhouse-server -u user1 --password 123456 +``` + +## Source + +- https://clickhouse.yandex/docs/en/operations/table_engines/replication/#creating-replicated-tables diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml new file mode 100644 index 000000000000..94c854dc273a --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_config.xml @@ -0,0 +1,370 @@ + + + + + error + 1000M + 1 + 10 + + + + 8123 + 9000 + + + + + + + + + /etc/clickhouse-server/server.crt + /etc/clickhouse-server/server.key + + /etc/clickhouse-server/dhparam.pem + none + true + true + sslv2,sslv3 + true + + + + true + true + sslv2,sslv3 + true + + + + RejectCertificateHandler + + + + + + + + + 9009 + + + + + + + + + + + + + + + + + + + + 4096 + 3 + + + 100 + + + + + + 8589934592 + + + 5368709120 + + + + /var/lib/clickhouse/ + + + /var/lib/clickhouse/tmp/ + + + /var/lib/clickhouse/user_files/ + + + users.xml + + + default + + + + + + default + + + + + + + + + + + + + + localhost + 9000 + + + + + + + localhost + 9440 + 1 + + + + + + + + /etc/clickhouse-server/metrika.xml + + + + + + + + + 3600 + + + + 3600 + + + 60 + + + + + + + + + + system + query_log
+ + toYYYYMM(event_date) + + 7500 +
+ + + + + + + + + + + + + + + + *_dictionary.xml + + + + + + + + + + /clickhouse/task_queue/ddl + + + + + + + + + + + + + + + + click_cost + any + + 0 + 3600 + + + 86400 + 60 + + + + max + + 0 + 60 + + + 3600 + 300 + + + 86400 + 3600 + + + + + + /var/lib/clickhouse/format_schemas/ + + + +
+ diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml new file mode 100644 index 000000000000..b58ffc34bc29 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/clickhouse_metrika.xml @@ -0,0 +1,60 @@ + + + + + 1 + true + + clickhouse-01 + 9000 + + + clickhouse-06 + 9000 + + + + 1 + true + + clickhouse-02 + 9000 + + + clickhouse-03 + 9000 + + + + 1 + true + + + clickhouse-04 + 9000 + + + clickhouse-05 + 9000 + + + + + + + clickhouse-zookeeper + 2181 + + + + ::/0 + + + + 10000000000 + 0.01 + lz4 + + + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml new file mode 100644 index 000000000000..75df1c5916e8 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-01.xml @@ -0,0 +1,9 @@ + + + clickhouse-01 + 01 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml new file mode 100644 index 000000000000..67e4a545b30c --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-02.xml @@ -0,0 +1,9 @@ + + + clickhouse-02 + 02 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml new file mode 100644 index 000000000000..e9278191b80f --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-03.xml @@ -0,0 +1,9 @@ + + + clickhouse-03 + 02 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml new file mode 100644 index 000000000000..033c0ad1152e --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-04.xml @@ -0,0 +1,9 @@ + + + clickhouse-04 + 03 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml new file mode 100644 index 000000000000..c63314c5acea --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-05.xml @@ -0,0 +1,9 @@ + + + clickhouse-05 + 03 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml new file mode 100644 index 000000000000..4b01bda9948c --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/macros/macros-06.xml @@ -0,0 +1,9 @@ + + + clickhouse-06 + 01 + 01 + data + cluster_1 + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml b/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml new file mode 100644 index 000000000000..e1b8de78e37a --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/config/users.xml @@ -0,0 +1,117 @@ + + + + + + + + 10000000000 + + + 0 + + + random + + + + + 1 + + + + + + + 123456 + + ::/0 + + default + default + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + ::1 + 127.0.0.1 + + readonly + default + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml b/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml new file mode 100644 index 000000000000..96d7618b47e6 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/docker-compose.yml @@ -0,0 +1,198 @@ +version: '3' + +networks: + ckh_net: + +services: + clickhouse-zookeeper: + image: zookeeper + ports: + - "2181:2181" + - "2182:2182" + container_name: clickhouse-zookeeper + hostname: clickhouse-zookeeper + networks: + - ckh_net + + clickhouse-01: + image: clickhouse/clickhouse-server + hostname: clickhouse-01 + container_name: clickhouse-01 + networks: + - ckh_net + ports: + - 9001:9000 + - 8124:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-01:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-02: + image: clickhouse/clickhouse-server + hostname: clickhouse-02 + container_name: clickhouse-02 + networks: + - ckh_net + ports: + - 9002:9000 + - 8125:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-02:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-03: + image: clickhouse/clickhouse-server + hostname: clickhouse-03 + container_name: clickhouse-03 + networks: + - ckh_net + ports: + - 9003:9000 + - 8126:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-03.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-03:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-04: + image: clickhouse/clickhouse-server + hostname: clickhouse-04 + container_name: clickhouse-04 + networks: + - ckh_net + ports: + - 9004:9000 + - 8127:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-04.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-04:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-05: + image: clickhouse/clickhouse-server + hostname: clickhouse-05 + container_name: clickhouse-05 + networks: + - ckh_net + ports: + - 9005:9000 + - 8128:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-05.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-05:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + clickhouse-06: + image: clickhouse/clickhouse-server + hostname: clickhouse-06 + container_name: clickhouse-06 + networks: + - ckh_net + ports: + - 9006:9000 + - 8129:8123 + volumes: + - ./config/clickhouse_config.xml:/etc/clickhouse-server/config.xml + - ./config/clickhouse_metrika.xml:/etc/clickhouse-server/metrika.xml + - ./config/macros/macros-06.xml:/etc/clickhouse-server/config.d/macros.xml + - ./config/users.xml:/etc/clickhouse-server/users.xml + # - ./data/server-06:/var/lib/clickhouse + ulimits: + nofile: + soft: 262144 + hard: 262144 + depends_on: + - "clickhouse-zookeeper" + + kafka0: + image: confluentinc/cp-kafka:7.0.5 + hostname: kafka0 + container_name: kafka0 + ports: + - 9092:9092 + - 9093 + - 9997 + - 29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + volumes: + - ./kafka-script.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + networks: + ckh_net: + aliases: + - hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local + + + # Kafka UI for debugging kafka queues + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8090:8080 + depends_on: + - kafka0 + networks: + - ckh_net + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_JMXPORT: 9997 + diff --git a/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh b/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh new file mode 100755 index 000000000000..023c832b4e1b --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/kafka-script.sh @@ -0,0 +1,11 @@ +# This script is required to run kafka cluster (without zookeeper) +#!/bin/sh + +# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter +sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure + +# Docker workaround: Ignore cub zk-ready +sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure + +# KRaft required step: Format the storage directory with a new cluster ID +echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql new file mode 100644 index 000000000000..0fe194a0e676 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql @@ -0,0 +1,237 @@ +CREATE TABLE hyperswitch.api_events_queue on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `created_at` DateTime CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) ENGINE = Kafka SETTINGS kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', +kafka_topic_list = 'hyperswitch-api-log-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String), + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_name TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/api_events_clustered', + '{replica}' +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_name) +TTL created_at + toIntervalMonth(6) +; + + +CREATE TABLE hyperswitch.api_events_dist on cluster '{cluster}' ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'api_events_clustered', rand()); + +CREATE MATERIALIZED VIEW hyperswitch.api_events_mv on cluster '{cluster}' TO hyperswitch.api_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) AS +SELECT + merchant_id, + payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + request_id, + flow_type, + api_name, + request, + response, + status_code, + url_path, + event_type, + now() as inserted_at, + created_at, + latency, + user_agent, + ip_addr +FROM + hyperswitch.api_events_queue +WHERE length(_error) = 0; + + +CREATE MATERIALIZED VIEW hyperswitch.api_events_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.api_events_queue +WHERE length(_error) > 0 +; + + +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `url_path` LowCardinality(Nullable(String)); +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `event_type` LowCardinality(Nullable(String)); + + +CREATE TABLE hyperswitch.api_audit_log ON CLUSTER '{cluster}' ( + `merchant_id` LowCardinality(String), + `payment_id` String, + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String), + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `customer_id` LowCardinality(Nullable(String)) +) ENGINE = ReplicatedMergeTree( '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/api_audit_log', '{replica}' ) PARTITION BY merchant_id +ORDER BY (merchant_id, payment_id) +TTL created_at + toIntervalMonth(18) +SETTINGS index_granularity = 8192 + + +CREATE MATERIALIZED VIEW hyperswitch.api_audit_log_mv ON CLUSTER `{cluster}` TO hyperswitch.api_audit_log( + `merchant_id` LowCardinality(String), + `payment_id` String, + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `request_id` Nullable(String), + `flow_type` LowCardinality(String), + `api_name` LowCardinality(String), + `request` String, + `response` String, + `status_code` UInt32, + `url_path` LowCardinality(Nullable(String)), + `event_type` LowCardinality(Nullable(String)), + `inserted_at` DateTime64(3), + `created_at` DateTime64(3), + `latency` Nullable(UInt128), + `user_agent` Nullable(String), + `ip_addr` Nullable(String) +) AS +SELECT + merchant_id, + multiIf(payment_id IS NULL, '', payment_id) AS payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + request_id, + flow_type, + api_name, + request, + response, + status_code, + url_path, + api_event_type AS event_type, + now() AS inserted_at, + created_at, + latency, + user_agent, + ip_addr +FROM hyperswitch.api_events_queue +WHERE length(_error) = 0 \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql new file mode 100644 index 000000000000..3a6281ae9050 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_attempts.sql @@ -0,0 +1,217 @@ +CREATE TABLE hyperswitch.payment_attempt_queue on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` LowCardinality(Nullable(String)), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-attempt-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE hyperswitch.payment_attempt_dist on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'payment_attempt_clustered', cityHash64(attempt_id)); + + + +CREATE MATERIALIZED VIEW hyperswitch.payment_attempt_mv on cluster '{cluster}' TO hyperswitch.payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime64(3), + `capture_on` Nullable(DateTime64(3)), + `last_synced` Nullable(DateTime64(3)), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + attempt_id, + status, + amount, + currency, + connector, + save_to_locker, + error_message, + offer_amount, + surcharge_amount, + tax_amount, + payment_method_id, + payment_method, + payment_method_type, + connector_transaction_id, + capture_method, + confirm, + authentication_type, + cancellation_reason, + amount_to_capture, + mandate_id, + browser_info, + error_code, + connector_metadata, + payment_experience, + created_at, + capture_on, + last_synced, + modified_at, + now() as inserted_at, + sign_flag +FROM + hyperswitch.payment_attempt_queue +WHERE length(_error) = 0; + + +CREATE TABLE hyperswitch.payment_attempt_clustered on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX authenticationTypeIndex authentication_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/payment_attempt_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, attempt_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.payment_attempt_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.payment_attempt_queue +WHERE length(_error) > 0 +; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql new file mode 100644 index 000000000000..eb2d83140e92 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/payment_intents.sql @@ -0,0 +1,165 @@ +CREATE TABLE hyperswitch.payment_intents_queue on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` String, + `business_label` String, + `modified_at` DateTime, + `created_at` DateTime, + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-intent-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.payment_intents_dist on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'payment_intents_clustered', cityHash64(payment_id)); + +CREATE TABLE hyperswitch.payment_intents_clustered on cluster '{cluster}' ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector_id TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/payment_intents_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, payment_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.payment_intent_mv on cluster '{cluster}' TO hyperswitch.payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime64(3), + `created_at` DateTime64(3), + `last_synced` Nullable(DateTime64(3)), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + status, + amount, + currency, + amount_captured, + customer_id, + description, + return_url, + connector_id, + statement_descriptor_name, + statement_descriptor_suffix, + setup_future_usage, + off_session, + client_secret, + active_attempt_id, + business_country, + business_label, + modified_at, + created_at, + last_synced, + now() as inserted_at, + sign_flag +FROM hyperswitch.payment_intents_queue +WHERE length(_error) = 0; + +CREATE MATERIALIZED VIEW hyperswitch.payment_intent_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.payment_intents_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql new file mode 100644 index 000000000000..bf5f6e0e2405 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/refund_analytics.sql @@ -0,0 +1,173 @@ +CREATE TABLE hyperswitch.refund_queue on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime, + `modified_at` DateTime, + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-refund-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.refund_dist on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Distributed('{cluster}', 'hyperswitch', 'refund_clustered', cityHash64(refund_id)); + + + +CREATE TABLE hyperswitch.refund_clustered on cluster '{cluster}' ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX refundTypeIndex refund_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex refund_status TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedCollapsingMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/refund_clustered', + '{replica}', + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, refund_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW hyperswitch.kafka_parse_refund on cluster '{cluster}' TO hyperswitch.refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime64(3), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + internal_reference_id, + refund_id, + payment_id, + merchant_id, + connector_transaction_id, + connector, + connector_refund_id, + external_reference_id, + refund_type, + total_amount, + currency, + refund_amount, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + attempt_id, + description, + refund_reason, + refund_error_code, + created_at, + modified_at, + now() as inserted_at, + sign_flag +FROM hyperswitch.refund_queue +WHERE length(_error) = 0; + +CREATE MATERIALIZED VIEW hyperswitch.refund_parse_errors on cluster '{cluster}' +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM hyperswitch.refund_queue +WHERE length(_error) > 0 +; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql new file mode 100644 index 000000000000..37766392bc70 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/sdk_events.sql @@ -0,0 +1,156 @@ +CREATE TABLE hyperswitch.sdk_events_queue on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` LowCardinality(Nullable(String)), + `latency` Nullable(UInt32), + `timestamp` String, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) +) ENGINE = Kafka SETTINGS + kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', + kafka_topic_list = 'hyper-sdk-logs', + kafka_group_name = 'hyper-c1', + kafka_format = 'JSONEachRow', + kafka_handle_error_mode = 'stream'; + +CREATE TABLE hyperswitch.sdk_events_clustered on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool DEFAULT 1, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', + `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `inserted_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `latency` Nullable(UInt32) DEFAULT 0, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX eventIndex event_name TYPE bloom_filter GRANULARITY 1, + INDEX platformIndex platform TYPE bloom_filter GRANULARITY 1, + INDEX logTypeIndex log_type TYPE bloom_filter GRANULARITY 1, + INDEX categoryIndex category TYPE bloom_filter GRANULARITY 1, + INDEX sourceIndex source TYPE bloom_filter GRANULARITY 1, + INDEX componentIndex component TYPE bloom_filter GRANULARITY 1, + INDEX firstEventIndex first_event TYPE bloom_filter GRANULARITY 1 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/{installation}/{cluster}/tables/{shard}/hyperswitch/sdk_events_clustered', '{replica}' +) +PARTITION BY + toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id) +TTL + toDateTime(created_at) + toIntervalMonth(6) +SETTINGS + index_granularity = 8192 +; + +CREATE TABLE hyperswitch.sdk_events_dist on cluster '{cluster}' ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool DEFAULT 1, + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', + `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `inserted_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), + `latency` Nullable(UInt32) DEFAULT 0 +) ENGINE = Distributed( + '{cluster}', 'hyperswitch', 'sdk_events_clustered', rand() +); + +CREATE MATERIALIZED VIEW hyperswitch.sdk_events_mv on cluster '{cluster}' TO hyperswitch.sdk_events_dist ( + `payment_id` Nullable(String), + `merchant_id` String, + `remote_ip` Nullable(String), + `log_type` LowCardinality(Nullable(String)), + `event_name` LowCardinality(Nullable(String)), + `first_event` Bool, + `latency` Nullable(UInt32), + `browser_name` LowCardinality(Nullable(String)), + `browser_version` Nullable(String), + `platform` LowCardinality(Nullable(String)), + `source` LowCardinality(Nullable(String)), + `category` LowCardinality(Nullable(String)), + `version` LowCardinality(Nullable(String)), + `value` Nullable(String), + `component` LowCardinality(Nullable(String)), + `payment_method` LowCardinality(Nullable(String)), + `payment_experience` LowCardinality(Nullable(String)), + `created_at` DateTime64(3) +) AS +SELECT + payment_id, + merchant_id, + remote_ip, + log_type, + event_name, + multiIf(first_event = 'true', 1, 0) AS first_event, + latency, + browser_name, + browser_version, + platform, + source, + category, + version, + value, + component, + payment_method, + payment_experience, + toDateTime64(timestamp, 3) AS created_at +FROM + hyperswitch.sdk_events_queue +WHERE length(_error) = 0 +; + +CREATE MATERIALIZED VIEW hyperswitch.sdk_parse_errors on cluster '{cluster}' ( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) ENGINE = MergeTree + ORDER BY (topic, partition, offset) +SETTINGS + index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM + hyperswitch.sdk_events_queue +WHERE + length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql new file mode 100644 index 000000000000..202b94ac6040 --- /dev/null +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/seed_scripts.sql @@ -0,0 +1 @@ +create database hyperswitch on cluster '{cluster}'; \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql new file mode 100644 index 000000000000..b41a75fe67e5 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql @@ -0,0 +1,134 @@ +CREATE TABLE api_events_v2_queue ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-api-log-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE api_events_v2_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `inserted_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String, + INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, + INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = MergeTree +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_flow) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW api_events_v2_mv TO api_events_v2_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `payment_method_id` Nullable(String), + `payment_method` Nullable(String), + `payment_method_type` Nullable(String), + `customer_id` Nullable(String), + `user_id` Nullable(String), + `connector` Nullable(String), + `request_id` String, + `flow_type` LowCardinality(String), + `api_flow` LowCardinality(String), + `api_auth_type` LowCardinality(String), + `request` String, + `response` Nullable(String), + `authentication_data` Nullable(String), + `status_code` UInt32, + `created_at` DateTime CODEC(T64, LZ4), + `inserted_at` DateTime CODEC(T64, LZ4), + `latency` UInt128, + `user_agent` String, + `ip_addr` String +) AS +SELECT + merchant_id, + payment_id, + refund_id, + payment_method_id, + payment_method, + payment_method_type, + customer_id, + user_id, + connector, + request_id, + flow_type, + api_flow, + api_auth_type, + request, + response, + authentication_data, + status_code, + created_at, + now() as inserted_at, + latency, + user_agent, + ip_addr +FROM + api_events_v2_queue +where length(_error) = 0; + + +CREATE MATERIALIZED VIEW api_events_parse_errors +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM api_events_v2_queue +WHERE length(_error) > 0 +; diff --git a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql new file mode 100644 index 000000000000..276e311e57a9 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql @@ -0,0 +1,156 @@ +CREATE TABLE payment_attempts_queue ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` LowCardinality(Nullable(String)), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-attempt-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + +CREATE TABLE payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `capture_on` Nullable(DateTime) CODEC(T64, LZ4), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, + INDEX authenticationTypeIndex authentication_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, attempt_id) +TTL created_at + toIntervalMonth(6) +; + + +CREATE MATERIALIZED VIEW kafka_parse_pa TO payment_attempt_dist ( + `payment_id` String, + `merchant_id` String, + `attempt_id` String, + `status` LowCardinality(String), + `amount` Nullable(UInt32), + `currency` LowCardinality(Nullable(String)), + `connector` LowCardinality(Nullable(String)), + `save_to_locker` Nullable(Bool), + `error_message` Nullable(String), + `offer_amount` Nullable(UInt32), + `surcharge_amount` Nullable(UInt32), + `tax_amount` Nullable(UInt32), + `payment_method_id` Nullable(String), + `payment_method` LowCardinality(Nullable(String)), + `payment_method_type` LowCardinality(Nullable(String)), + `connector_transaction_id` Nullable(String), + `capture_method` Nullable(String), + `confirm` Bool, + `authentication_type` LowCardinality(Nullable(String)), + `cancellation_reason` Nullable(String), + `amount_to_capture` Nullable(UInt32), + `mandate_id` Nullable(String), + `browser_info` Nullable(String), + `error_code` Nullable(String), + `connector_metadata` Nullable(String), + `payment_experience` Nullable(String), + `created_at` DateTime64(3), + `capture_on` Nullable(DateTime64(3)), + `last_synced` Nullable(DateTime64(3)), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + attempt_id, + status, + amount, + currency, + connector, + save_to_locker, + error_message, + offer_amount, + surcharge_amount, + tax_amount, + payment_method_id, + payment_method, + payment_method_type, + connector_transaction_id, + capture_method, + confirm, + authentication_type, + cancellation_reason, + amount_to_capture, + mandate_id, + browser_info, + error_code, + connector_metadata, + payment_experience, + created_at, + capture_on, + last_synced, + modified_at, + now() as inserted_at, + sign_flag +FROM + payment_attempts_queue; + diff --git a/crates/analytics/docs/clickhouse/scripts/payment_intents.sql b/crates/analytics/docs/clickhouse/scripts/payment_intents.sql new file mode 100644 index 000000000000..8cd487f364b4 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/payment_intents.sql @@ -0,0 +1,116 @@ +CREATE TABLE payment_intents_queue ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` String, + `business_label` String, + `modified_at` DateTime CODEC(T64, LZ4), + `created_at` DateTime CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-payment-intent-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `last_synced` Nullable(DateTime) CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector_id TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, payment_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW kafka_parse_payment_intent TO payment_intents_dist ( + `payment_id` String, + `merchant_id` String, + `status` LowCardinality(String), + `amount` UInt32, + `currency` LowCardinality(Nullable(String)), + `amount_captured` Nullable(UInt32), + `customer_id` Nullable(String), + `description` Nullable(String), + `return_url` Nullable(String), + `connector_id` LowCardinality(Nullable(String)), + `statement_descriptor_name` Nullable(String), + `statement_descriptor_suffix` Nullable(String), + `setup_future_usage` LowCardinality(Nullable(String)), + `off_session` Nullable(Bool), + `client_secret` Nullable(String), + `active_attempt_id` String, + `business_country` LowCardinality(String), + `business_label` String, + `modified_at` DateTime64(3), + `created_at` DateTime64(3), + `last_synced` Nullable(DateTime64(3)), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + payment_id, + merchant_id, + status, + amount, + currency, + amount_captured, + customer_id, + description, + return_url, + connector_id, + statement_descriptor_name, + statement_descriptor_suffix, + setup_future_usage, + off_session, + client_secret, + active_attempt_id, + business_country, + business_label, + modified_at, + created_at, + last_synced, + now() as inserted_at, + sign_flag +FROM payment_intents_queue; diff --git a/crates/analytics/docs/clickhouse/scripts/refunds.sql b/crates/analytics/docs/clickhouse/scripts/refunds.sql new file mode 100644 index 000000000000..a131270c1326 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/refunds.sql @@ -0,0 +1,121 @@ +CREATE TABLE refund_queue ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime CODEC(T64, LZ4), + `modified_at` DateTime CODEC(T64, LZ4), + `sign_flag` Int8 +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-refund-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `modified_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `sign_flag` Int8, + INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, + INDEX refundTypeIndex refund_type TYPE bloom_filter GRANULARITY 1, + INDEX currencyIndex currency TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex refund_status TYPE bloom_filter GRANULARITY 1 +) ENGINE = CollapsingMergeTree( + sign_flag +) +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, refund_id) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW kafka_parse_refund TO refund_dist ( + `internal_reference_id` String, + `refund_id` String, + `payment_id` String, + `merchant_id` String, + `connector_transaction_id` String, + `connector` LowCardinality(Nullable(String)), + `connector_refund_id` Nullable(String), + `external_reference_id` Nullable(String), + `refund_type` LowCardinality(String), + `total_amount` Nullable(UInt32), + `currency` LowCardinality(String), + `refund_amount` Nullable(UInt32), + `refund_status` LowCardinality(String), + `sent_to_gateway` Bool, + `refund_error_message` Nullable(String), + `refund_arn` Nullable(String), + `attempt_id` String, + `description` Nullable(String), + `refund_reason` Nullable(String), + `refund_error_code` Nullable(String), + `created_at` DateTime64(3), + `modified_at` DateTime64(3), + `inserted_at` DateTime64(3), + `sign_flag` Int8 +) AS +SELECT + internal_reference_id, + refund_id, + payment_id, + merchant_id, + connector_transaction_id, + connector, + connector_refund_id, + external_reference_id, + refund_type, + total_amount, + currency, + refund_amount, + refund_status, + sent_to_gateway, + refund_error_message, + refund_arn, + attempt_id, + description, + refund_reason, + refund_error_code, + created_at, + modified_at, + now() as inserted_at, + sign_flag +FROM refund_queue; diff --git a/crates/analytics/src/api_event.rs b/crates/analytics/src/api_event.rs new file mode 100644 index 000000000000..113344d47254 --- /dev/null +++ b/crates/analytics/src/api_event.rs @@ -0,0 +1,9 @@ +mod core; +pub mod events; +pub mod filters; +pub mod metrics; +pub mod types; + +pub trait APIEventAnalytics: events::ApiLogsFilterAnalytics {} + +pub use self::core::{api_events_core, get_api_event_metrics, get_filters}; diff --git a/crates/analytics/src/api_event/core.rs b/crates/analytics/src/api_event/core.rs new file mode 100644 index 000000000000..b368d6374f75 --- /dev/null +++ b/crates/analytics/src/api_event/core.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + api_event::{ + ApiEventMetricsBucketIdentifier, ApiEventMetricsBucketValue, ApiLogsRequest, + ApiMetricsBucketResponse, + }, + AnalyticsMetadata, ApiEventFiltersResponse, GetApiEventFiltersRequest, + GetApiEventMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::{ + events::{get_api_event, ApiLogsResult}, + metrics::ApiEventMetricRow, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + types::FiltersError, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn api_events_core( + pool: &AnalyticsProvider, + req: ApiLogsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Clickhouse(pool) => get_api_event(&merchant_id, req, pool).await, + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_api_event(&merchant_id, req, ckh_pool).await + } + } + .change_context(AnalyticsError::UnknownError)?; + Ok(data) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetApiEventFiltersRequest, + merchant_id: String, +) -> AnalyticsResult { + use api_models::analytics::{api_event::ApiEventDimensions, ApiEventFilterValue}; + + use super::filters::get_api_event_filter_for_dimension; + use crate::api_event::filters::ApiEventFilter; + + let mut res = ApiEventFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_api_event_filter_for_dimension(dim, &merchant_id, &req.time_range, ckh_pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: ApiEventFilter| match dim { + ApiEventDimensions::StatusCode => fil.status_code.map(|i| i.to_string()), + ApiEventDimensions::FlowType => fil.flow_type, + ApiEventDimensions::ApiFlow => fil.api_flow, + }) + .collect::>(); + res.query_data.push(ApiEventFilterValue { + dimension: dim, + values, + }) + } + + Ok(res) +} + +#[instrument(skip_all)] +pub async fn get_api_event_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetApiEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_api_metrics_query", + api_event_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_api_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + for (id, value) in data { + metrics_accumulator + .entry(id) + .and_modify(|data| { + data.api_count = data.api_count.or(value.api_count); + data.status_code_count = data.status_code_count.or(value.status_code_count); + data.latency = data.latency.or(value.latency); + }) + .or_insert(value); + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| ApiMetricsBucketResponse { + values: ApiEventMetricsBucketValue { + latency: val.latency, + api_count: val.api_count, + status_code_count: val.status_code_count, + }, + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} diff --git a/crates/analytics/src/api_event/events.rs b/crates/analytics/src/api_event/events.rs new file mode 100644 index 000000000000..73b3fb9cbad2 --- /dev/null +++ b/crates/analytics/src/api_event/events.rs @@ -0,0 +1,105 @@ +use api_models::analytics::{ + api_event::{ApiLogsRequest, QueryType}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use router_env::Flow; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait ApiLogsFilterAnalytics: LoadRow {} + +pub async fn get_api_event( + merchant_id: &String, + query_param: ApiLogsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ApiLogsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + match query_param.query_param { + QueryType::Payment { payment_id } => query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?, + QueryType::Refund { + payment_id, + refund_id, + } => { + query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?; + query_builder + .add_filter_clause("refund_id", refund_id) + .switch()?; + } + } + if let Some(list_api_name) = query_param.api_name_filter { + query_builder + .add_filter_in_range_clause("api_flow", &list_api_name) + .switch()?; + } else { + query_builder + .add_filter_in_range_clause( + "api_flow", + &[ + Flow::PaymentsCancel, + Flow::PaymentsCapture, + Flow::PaymentsConfirm, + Flow::PaymentsCreate, + Flow::PaymentsStart, + Flow::PaymentsUpdate, + Flow::RefundsCreate, + Flow::IncomingWebhookReceive, + ], + ) + .switch()?; + } + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ApiLogsResult { + pub merchant_id: String, + pub payment_id: Option, + pub refund_id: Option, + pub payment_method_id: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub customer_id: Option, + pub user_id: Option, + pub connector: Option, + pub request_id: Option, + pub flow_type: String, + pub api_flow: String, + pub api_auth_type: Option, + pub request: String, + pub response: Option, + pub error: Option, + pub authentication_data: Option, + pub status_code: u16, + pub latency: Option, + pub user_agent: Option, + pub hs_latency: Option, + pub ip_addr: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/api_event/filters.rs b/crates/analytics/src/api_event/filters.rs new file mode 100644 index 000000000000..87414ebad4ba --- /dev/null +++ b/crates/analytics/src/api_event/filters.rs @@ -0,0 +1,53 @@ +use api_models::analytics::{api_event::ApiEventDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; + +pub trait ApiEventFilterAnalytics: LoadRow {} + +pub async fn get_api_event_filter_for_dimension( + dimension: ApiEventDimensions, + merchant_id: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ApiEventFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct ApiEventFilter { + pub status_code: Option, + pub flow_type: Option, + pub api_flow: Option, +} diff --git a/crates/analytics/src/api_event/metrics.rs b/crates/analytics/src/api_event/metrics.rs new file mode 100644 index 000000000000..16f2d7a2f5ab --- /dev/null +++ b/crates/analytics/src/api_event/metrics.rs @@ -0,0 +1,110 @@ +use api_models::analytics::{ + api_event::{ + ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod api_count; +pub mod latency; +mod status_code_count; +use api_count::ApiCount; +use latency::MaxLatency; +use status_code_count::StatusCodeCount; + +use self::latency::LatencyAvg; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct ApiEventMetricRow { + pub latency: Option, + pub api_count: Option, + pub status_code_count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait ApiEventMetricAnalytics: LoadRow + LoadRow {} + +#[async_trait::async_trait] +pub trait ApiEventMetric +where + T: AnalyticsDataSource + ApiEventMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl ApiEventMetric for ApiEventMetrics +where + T: AnalyticsDataSource + ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::Latency => { + MaxLatency + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::ApiCount => { + ApiCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::StatusCodeCount => { + StatusCodeCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/api_event/metrics/api_count.rs b/crates/analytics/src/api_event/metrics/api_count.rs new file mode 100644 index 000000000000..7f5f291aa53e --- /dev/null +++ b/crates/analytics/src/api_event/metrics/api_count.rs @@ -0,0 +1,106 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ApiCount; + +#[async_trait::async_trait] +impl super::ApiEventMetric for ApiCount +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("api_count"), + }) + .switch()?; + if !filters.flow_type.is_empty() { + query_builder + .add_filter_in_range_clause(ApiEventDimensions::FlowType, &filters.flow_type) + .attach_printable("Error adding flow_type filter") + .switch()?; + } + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/api_event/metrics/latency.rs b/crates/analytics/src/api_event/metrics/latency.rs new file mode 100644 index 000000000000..379b39fbeb9e --- /dev/null +++ b/crates/analytics/src/api_event/metrics/latency.rs @@ -0,0 +1,138 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct MaxLatency; + +#[async_trait::async_trait] +impl super::ApiEventMetric for MaxLatency +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Sum { + field: "latency", + alias: Some("latency_sum"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: Some("latency"), + alias: Some("latency_count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_custom_filter_clause("request", "10.63.134.6", FilterTypes::NotLike) + .attach_printable("Error filtering out locker IP") + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + ApiEventMetricRow { + latency: if i.latency_count != 0 { + Some(i.latency_sum.unwrap_or(0) / i.latency_count) + } else { + None + }, + api_count: None, + status_code_count: None, + start_bucket: i.start_bucket, + end_bucket: i.end_bucket, + }, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct LatencyAvg { + latency_sum: Option, + latency_count: u64, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} diff --git a/crates/analytics/src/api_event/metrics/status_code_count.rs b/crates/analytics/src/api_event/metrics/status_code_count.rs new file mode 100644 index 000000000000..5c652fd8e0c9 --- /dev/null +++ b/crates/analytics/src/api_event/metrics/status_code_count.rs @@ -0,0 +1,103 @@ +use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventFilters, ApiEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::ApiEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct StatusCodeCount; + +#[async_trait::async_trait] +impl super::ApiEventMetric for StatusCodeCount +where + T: AnalyticsDataSource + super::ApiEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[ApiEventDimensions], + merchant_id: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::ApiEvents); + + query_builder + .add_select_column(Aggregate::Count { + field: Some("status_code"), + alias: Some("status_code_count"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + ApiEventMetricsBucketIdentifier::new(TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/api_event/types.rs b/crates/analytics/src/api_event/types.rs new file mode 100644 index 000000000000..72205fc72abf --- /dev/null +++ b/crates/analytics/src/api_event/types.rs @@ -0,0 +1,33 @@ +use api_models::analytics::api_event::{ApiEventDimensions, ApiEventFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for ApiEventFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.status_code.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::StatusCode, &self.status_code) + .attach_printable("Error adding status_code filter")?; + } + if !self.flow_type.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::FlowType, &self.flow_type) + .attach_printable("Error adding flow_type filter")?; + } + if !self.api_flow.is_empty() { + builder + .add_filter_in_range_clause(ApiEventDimensions::ApiFlow, &self.api_flow) + .attach_printable("Error adding api_name filter")?; + } + + Ok(()) + } +} diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs new file mode 100644 index 000000000000..964486c93649 --- /dev/null +++ b/crates/analytics/src/clickhouse.rs @@ -0,0 +1,458 @@ +use std::sync::Arc; + +use actix_web::http::StatusCode; +use common_utils::errors::ParsingError; +use error_stack::{IntoReport, Report, ResultExt}; +use router_env::logger; +use time::PrimitiveDateTime; + +use super::{ + payments::{ + distribution::PaymentDistributionRow, filters::FilterRow, metrics::PaymentMetricRow, + }, + query::{Aggregate, ToSql, Window}, + refunds::{filters::RefundFilterRow, metrics::RefundMetricRow}, + sdk_events::{filters::SdkEventFilter, metrics::SdkEventMetricRow}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, QueryExecutionError}, +}; +use crate::{ + api_event::{ + events::ApiLogsResult, + filters::ApiEventFilter, + metrics::{latency::LatencyAvg, ApiEventMetricRow}, + }, + sdk_events::events::SdkEventsResult, + types::TableEngine, +}; + +pub type ClickhouseResult = error_stack::Result; + +#[derive(Clone, Debug)] +pub struct ClickhouseClient { + pub config: Arc, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct ClickhouseConfig { + username: String, + password: Option, + host: String, + database_name: String, +} + +impl Default for ClickhouseConfig { + fn default() -> Self { + Self { + username: "default".to_string(), + password: None, + host: "http://localhost:8123".to_string(), + database_name: "default".to_string(), + } + } +} + +impl ClickhouseClient { + async fn execute_query(&self, query: &str) -> ClickhouseResult> { + logger::debug!("Executing query: {query}"); + let client = reqwest::Client::new(); + let params = CkhQuery { + date_time_output_format: String::from("iso"), + output_format_json_quote_64bit_integers: 0, + database: self.config.database_name.clone(), + }; + let response = client + .post(&self.config.host) + .query(¶ms) + .basic_auth(self.config.username.clone(), self.config.password.clone()) + .body(format!("{query}\nFORMAT JSON")) + .send() + .await + .into_report() + .change_context(ClickhouseError::ConnectionError)?; + + logger::debug!(clickhouse_response=?response, query=?query, "Clickhouse response"); + if response.status() != StatusCode::OK { + response.text().await.map_or_else( + |er| { + Err(ClickhouseError::ResponseError) + .into_report() + .attach_printable_lazy(|| format!("Error: {er:?}")) + }, + |t| Err(ClickhouseError::ResponseNotOK(t)).into_report(), + ) + } else { + Ok(response + .json::>() + .await + .into_report() + .change_context(ClickhouseError::ResponseError)? + .data) + } + } +} + +#[async_trait::async_trait] +impl AnalyticsDataSource for ClickhouseClient { + type Row = serde_json::Value; + + async fn load_results( + &self, + query: &str, + ) -> common_utils::errors::CustomResult, QueryExecutionError> + where + Self: LoadRow, + { + self.execute_query(query) + .await + .change_context(QueryExecutionError::DatabaseError)? + .into_iter() + .map(Self::load_row) + .collect::, _>>() + .change_context(QueryExecutionError::RowExtractionFailure) + } + + fn get_table_engine(table: AnalyticsCollection) -> TableEngine { + match table { + AnalyticsCollection::Payment + | AnalyticsCollection::Refund + | AnalyticsCollection::PaymentIntent => { + TableEngine::CollapsingMergeTree { sign: "sign_flag" } + } + AnalyticsCollection::SdkEvents => TableEngine::BasicTree, + AnalyticsCollection::ApiEvents => TableEngine::BasicTree, + } + } +} + +impl LoadRow for ClickhouseClient +where + Self::Row: TryInto>, +{ + fn load_row(row: Self::Row) -> common_utils::errors::CustomResult { + row.try_into() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl super::payments::filters::PaymentFilterAnalytics for ClickhouseClient {} +impl super::payments::metrics::PaymentMetricAnalytics for ClickhouseClient {} +impl super::payments::distribution::PaymentDistributionAnalytics for ClickhouseClient {} +impl super::refunds::metrics::RefundMetricAnalytics for ClickhouseClient {} +impl super::refunds::filters::RefundFilterAnalytics for ClickhouseClient {} +impl super::sdk_events::filters::SdkEventFilterAnalytics for ClickhouseClient {} +impl super::sdk_events::metrics::SdkEventMetricAnalytics for ClickhouseClient {} +impl super::sdk_events::events::SdkEventsFilterAnalytics for ClickhouseClient {} +impl super::api_event::events::ApiLogsFilterAnalytics for ClickhouseClient {} +impl super::api_event::filters::ApiEventFilterAnalytics for ClickhouseClient {} +impl super::api_event::metrics::ApiEventMetricAnalytics for ClickhouseClient {} + +#[derive(Debug, serde::Serialize)] +struct CkhQuery { + date_time_output_format: String, + output_format_json_quote_64bit_integers: u8, + database: String, +} + +#[derive(Debug, serde::Deserialize)] +struct CkhOutput { + data: Vec, +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiLogsResult in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventsResult in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentDistributionRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse FilterRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse RefundMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse RefundFilterRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiEventMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse LatencyAvg in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventMetricRow in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse SdkEventFilter in clickhouse results", + )) + } +} + +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ApiEventFilter in clickhouse results", + )) + } +} + +impl ToSql for PrimitiveDateTime { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + let format = + time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]") + .into_report() + .change_context(ParsingError::DateTimeParsingError) + .attach_printable("Failed to parse format description")?; + self.format(&format) + .into_report() + .change_context(ParsingError::EncodeError( + "failed to encode to clickhouse date-time format", + )) + .attach_printable("Failed to format date time") + } +} + +impl ToSql for AnalyticsCollection { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { + match self { + Self::Payment => Ok("payment_attempt_dist".to_string()), + Self::Refund => Ok("refund_dist".to_string()), + Self::SdkEvents => Ok("sdk_events_dist".to_string()), + Self::ApiEvents => Ok("api_audit_log".to_string()), + Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + } + } +} + +impl ToSql for Aggregate +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Count { field: _, alias } => { + let query = match table_engine { + TableEngine::CollapsingMergeTree { sign } => format!("sum({sign})"), + TableEngine::BasicTree => "count(*)".to_string(), + }; + format!( + "{query}{}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Sum { field, alias } => { + let query = match table_engine { + TableEngine::CollapsingMergeTree { sign } => format!( + "sum({sign} * {})", + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")? + ), + TableEngine::BasicTree => format!( + "sum({})", + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")? + ), + }; + format!( + "{query}{}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Min { field, alias } => { + format!( + "min({}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to min aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Max { field, alias } => { + format!( + "max({}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +impl ToSql for Window +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Sum { + field, + partition_by, + order_by, + alias, + } => { + format!( + "sum({}) over ({}{}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to sum window")?, + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::RowNumber { + field: _, + partition_by, + order_by, + alias, + } => { + format!( + "row_number() over ({}{}){}", + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ClickhouseError { + #[error("Clickhouse connection error")] + ConnectionError, + #[error("Clickhouse NON-200 response content: '{0}'")] + ResponseNotOK(String), + #[error("Clickhouse response error")] + ResponseError, +} diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs new file mode 100644 index 000000000000..354e1e2f1766 --- /dev/null +++ b/crates/analytics/src/core.rs @@ -0,0 +1,31 @@ +use api_models::analytics::GetInfoResponse; + +use crate::{types::AnalyticsDomain, utils}; + +pub async fn get_domain_info( + domain: AnalyticsDomain, +) -> crate::errors::AnalyticsResult { + let info = match domain { + AnalyticsDomain::Payments => GetInfoResponse { + metrics: utils::get_payment_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_dimensions(), + }, + AnalyticsDomain::Refunds => GetInfoResponse { + metrics: utils::get_refund_metrics_info(), + download_dimensions: None, + dimensions: utils::get_refund_dimensions(), + }, + AnalyticsDomain::SdkEvents => GetInfoResponse { + metrics: utils::get_sdk_event_metrics_info(), + download_dimensions: None, + dimensions: utils::get_sdk_event_dimensions(), + }, + AnalyticsDomain::ApiEvents => GetInfoResponse { + metrics: utils::get_api_event_metrics_info(), + download_dimensions: None, + dimensions: utils::get_api_event_dimensions(), + }, + }; + Ok(info) +} diff --git a/crates/router/src/analytics/errors.rs b/crates/analytics/src/errors.rs similarity index 100% rename from crates/router/src/analytics/errors.rs rename to crates/analytics/src/errors.rs diff --git a/crates/analytics/src/lambda_utils.rs b/crates/analytics/src/lambda_utils.rs new file mode 100644 index 000000000000..f9446a402b4e --- /dev/null +++ b/crates/analytics/src/lambda_utils.rs @@ -0,0 +1,36 @@ +use aws_config::{self, meta::region::RegionProviderChain}; +use aws_sdk_lambda::{config::Region, types::InvocationType::Event, Client}; +use aws_smithy_types::Blob; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use crate::errors::AnalyticsError; + +async fn get_aws_client(region: String) -> Client { + let region_provider = RegionProviderChain::first_try(Region::new(region)); + let sdk_config = aws_config::from_env().region(region_provider).load().await; + Client::new(&sdk_config) +} + +pub async fn invoke_lambda( + function_name: &str, + region: &str, + json_bytes: &[u8], +) -> CustomResult<(), AnalyticsError> { + get_aws_client(region.to_string()) + .await + .invoke() + .function_name(function_name) + .invocation_type(Event) + .payload(Blob::new(json_bytes.to_owned())) + .send() + .await + .into_report() + .map_err(|er| { + let er_rep = format!("{er:?}"); + er.attach_printable(er_rep) + }) + .change_context(AnalyticsError::UnknownError) + .attach_printable("Lambda invocation failed")?; + Ok(()) +} diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs new file mode 100644 index 000000000000..24da77f84f2b --- /dev/null +++ b/crates/analytics/src/lib.rs @@ -0,0 +1,509 @@ +mod clickhouse; +pub mod core; +pub mod errors; +pub mod metrics; +pub mod payments; +mod query; +pub mod refunds; + +pub mod api_event; +pub mod sdk_events; +mod sqlx; +mod types; +use api_event::metrics::{ApiEventMetric, ApiEventMetricRow}; +pub use types::AnalyticsDomain; +pub mod lambda_utils; +pub mod utils; + +use std::sync::Arc; + +use api_models::analytics::{ + api_event::{ + ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, + }, + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, + }, + Distribution, Granularity, TimeRange, +}; +use clickhouse::ClickhouseClient; +pub use clickhouse::ClickhouseConfig; +use error_stack::IntoReport; +use router_env::{ + logger, + tracing::{self, instrument}, +}; +use storage_impl::config::Database; + +use self::{ + payments::{ + distribution::{PaymentDistribution, PaymentDistributionRow}, + metrics::{PaymentMetric, PaymentMetricRow}, + }, + refunds::metrics::{RefundMetric, RefundMetricRow}, + sdk_events::metrics::{SdkEventMetric, SdkEventMetricRow}, + sqlx::SqlxClient, + types::MetricsError, +}; + +#[derive(Clone, Debug)] +pub enum AnalyticsProvider { + Sqlx(SqlxClient), + Clickhouse(ClickhouseClient), + CombinedCkh(SqlxClient, ClickhouseClient), + CombinedSqlx(SqlxClient, ClickhouseClient), +} + +impl Default for AnalyticsProvider { + fn default() -> Self { + Self::Sqlx(SqlxClient::default()) + } +} + +impl ToString for AnalyticsProvider { + fn to_string(&self) -> String { + String::from(match self { + Self::Clickhouse(_) => "Clickhouse", + Self::Sqlx(_) => "Sqlx", + Self::CombinedCkh(_, _) => "CombinedCkh", + Self::CombinedSqlx(_, _) => "CombinedSqlx", + }) + } +} + +impl AnalyticsProvider { + #[instrument(skip_all)] + pub async fn get_payment_metrics( + &self, + metric: &PaymentMetrics, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics metrics") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics metrics") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_payment_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics distribution") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + distribution.distribution_for + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics distribution") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + &distribution.distribution_for, + self, + ) + .await + } + + pub async fn get_refund_metrics( + &self, + metric: &RefundMetrics, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each refund metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics metrics") + } + _ => {} + }; + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!( + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric.load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + ) + ); + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics metrics") + } + _ => {} + }; + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_sdk_event_metrics( + &self, + metric: &SdkEventMetrics, + dimensions: &[SdkEventDimensions], + pub_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(MetricsError::NotImplemented).into_report(), + Self::Clickhouse(pool) => { + metric + .load_metrics(dimensions, pub_key, filters, granularity, time_range, pool) + .await + } + Self::CombinedCkh(_sqlx_pool, ckh_pool) | Self::CombinedSqlx(_sqlx_pool, ckh_pool) => { + metric + .load_metrics( + dimensions, + pub_key, + filters, + granularity, + // Since SDK events are ckh only use ckh here + time_range, + ckh_pool, + ) + .await + } + } + } + + pub async fn get_api_event_metrics( + &self, + metric: &ApiEventMetrics, + dimensions: &[ApiEventDimensions], + pub_key: &str, + filters: &ApiEventFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(MetricsError::NotImplemented).into_report(), + Self::Clickhouse(ckh_pool) + | Self::CombinedCkh(_, ckh_pool) + | Self::CombinedSqlx(_, ckh_pool) => { + // Since API events are ckh only use ckh here + metric + .load_metrics( + dimensions, + pub_key, + filters, + granularity, + time_range, + ckh_pool, + ) + .await + } + } + } + + pub async fn from_conf(config: &AnalyticsConfig) -> Self { + match config { + AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx(SqlxClient::from_conf(sqlx).await), + AnalyticsConfig::Clickhouse { clickhouse } => Self::Clickhouse(ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }), + AnalyticsConfig::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh( + SqlxClient::from_conf(sqlx).await, + ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }, + ), + AnalyticsConfig::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx( + SqlxClient::from_conf(sqlx).await, + ClickhouseClient { + config: Arc::new(clickhouse.clone()), + }, + ), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum AnalyticsConfig { + Sqlx { + sqlx: Database, + }, + Clickhouse { + clickhouse: ClickhouseConfig, + }, + CombinedCkh { + sqlx: Database, + clickhouse: ClickhouseConfig, + }, + CombinedSqlx { + sqlx: Database, + clickhouse: ClickhouseConfig, + }, +} + +impl Default for AnalyticsConfig { + fn default() -> Self { + Self::Sqlx { + sqlx: Database::default(), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, Default, serde::Serialize)] +pub struct ReportConfig { + pub payment_function: String, + pub refund_function: String, + pub dispute_function: String, + pub region: String, +} diff --git a/crates/analytics/src/main.rs b/crates/analytics/src/main.rs new file mode 100644 index 000000000000..5bf256ea9783 --- /dev/null +++ b/crates/analytics/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello world"); +} diff --git a/crates/router/src/analytics/metrics.rs b/crates/analytics/src/metrics.rs similarity index 100% rename from crates/router/src/analytics/metrics.rs rename to crates/analytics/src/metrics.rs diff --git a/crates/router/src/analytics/metrics/request.rs b/crates/analytics/src/metrics/request.rs similarity index 51% rename from crates/router/src/analytics/metrics/request.rs rename to crates/analytics/src/metrics/request.rs index b7c202f2db25..3d1a78808f34 100644 --- a/crates/router/src/analytics/metrics/request.rs +++ b/crates/analytics/src/metrics/request.rs @@ -6,24 +6,20 @@ pub fn add_attributes>( } #[inline] -pub async fn record_operation_time( +pub async fn record_operation_time( future: F, metric: &once_cell::sync::Lazy>, - metric_name: &api_models::analytics::payments::PaymentMetrics, - source: &crate::analytics::AnalyticsProvider, + metric_name: &T, + source: &crate::AnalyticsProvider, ) -> R where F: futures::Future, + T: ToString, { let (result, time) = time_future(future).await; let attributes = &[ add_attributes("metric_name", metric_name.to_string()), - add_attributes( - "source", - match source { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), + add_attributes("source", source.to_string()), ]; let value = time.as_secs_f64(); metric.record(&super::CONTEXT, value, attributes); @@ -44,17 +40,3 @@ where let time_spent = start.elapsed(); (result, time_spent) } - -#[macro_export] -macro_rules! histogram_metric { - ($name:ident, $meter:ident) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); - }; - ($name:ident, $meter:ident, $description:literal) => { - pub(crate) static $name: once_cell::sync::Lazy< - $crate::opentelemetry::metrics::Histogram, - > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); - }; -} diff --git a/crates/analytics/src/payments.rs b/crates/analytics/src/payments.rs new file mode 100644 index 000000000000..984647172c5b --- /dev/null +++ b/crates/analytics/src/payments.rs @@ -0,0 +1,16 @@ +pub mod accumulator; +mod core; +pub mod distribution; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{ + PaymentDistributionAccumulator, PaymentMetricAccumulator, PaymentMetricsAccumulator, +}; + +pub trait PaymentAnalytics: + metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/router/src/analytics/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs similarity index 62% rename from crates/router/src/analytics/payments/accumulator.rs rename to crates/analytics/src/payments/accumulator.rs index 5eebd0974693..c340f2888f8b 100644 --- a/crates/router/src/analytics/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -1,8 +1,9 @@ -use api_models::analytics::payments::PaymentMetricsBucketValue; -use common_enums::enums as storage_enums; +use api_models::analytics::payments::{ErrorResult, PaymentMetricsBucketValue}; +use bigdecimal::ToPrimitive; +use diesel_models::enums as storage_enums; use router_env::logger; -use super::metrics::PaymentMetricRow; +use super::{distribution::PaymentDistributionRow, metrics::PaymentMetricRow}; #[derive(Debug, Default)] pub struct PaymentMetricsAccumulator { @@ -11,6 +12,22 @@ pub struct PaymentMetricsAccumulator { pub payment_success: CountAccumulator, pub processed_amount: SumAccumulator, pub avg_ticket_size: AverageAccumulator, + pub payment_error_message: ErrorDistributionAccumulator, + pub retries_count: CountAccumulator, + pub retries_amount_processed: SumAccumulator, + pub connector_success_rate: SuccessRateAccumulator, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionRow { + pub count: i64, + pub total: i64, + pub error_message: String, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionAccumulator { + pub error_vec: Vec, } #[derive(Debug, Default)] @@ -45,6 +62,51 @@ pub trait PaymentMetricAccumulator { fn collect(self) -> Self::MetricOutput; } +pub trait PaymentDistributionAccumulator { + type DistributionOutput; + + fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow); + + fn collect(self) -> Self::DistributionOutput; +} + +impl PaymentDistributionAccumulator for ErrorDistributionAccumulator { + type DistributionOutput = Option>; + + fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow) { + self.error_vec.push(ErrorDistributionRow { + count: distribution.count.unwrap_or_default(), + total: distribution + .total + .clone() + .map(|i| i.to_i64().unwrap_or_default()) + .unwrap_or_default(), + error_message: distribution.error_message.clone().unwrap_or("".to_string()), + }) + } + + fn collect(mut self) -> Self::DistributionOutput { + if self.error_vec.is_empty() { + None + } else { + self.error_vec.sort_by(|a, b| b.count.cmp(&a.count)); + let mut res: Vec = Vec::new(); + for val in self.error_vec.into_iter() { + let perc = f64::from(u32::try_from(val.count).ok()?) * 100.0 + / f64::from(u32::try_from(val.total).ok()?); + + res.push(ErrorResult { + reason: val.error_message, + count: val.count, + percentage: (perc * 100.0).round() / 100.0, + }) + } + + Some(res) + } + } +} + impl PaymentMetricAccumulator for SuccessRateAccumulator { type MetricOutput = Option; @@ -145,6 +207,10 @@ impl PaymentMetricsAccumulator { payment_success_count: self.payment_success.collect(), payment_processed_amount: self.processed_amount.collect(), avg_ticket_size: self.avg_ticket_size.collect(), + payment_error_message: self.payment_error_message.collect(), + retries_count: self.retries_count.collect(), + retries_amount_processed: self.retries_amount_processed.collect(), + connector_success_rate: self.connector_success_rate.collect(), } } } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs new file mode 100644 index 000000000000..138e88789327 --- /dev/null +++ b/crates/analytics/src/payments/core.rs @@ -0,0 +1,303 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + payments::{ + MetricsBucketResponse, PaymentDimensions, PaymentDistributions, PaymentMetrics, + PaymentMetricsBucketIdentifier, + }, + AnalyticsMetadata, FilterValue, GetPaymentFiltersRequest, GetPaymentMetricRequest, + MetricsResponse, PaymentFiltersResponse, +}; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::{ + distribution::PaymentDistributionRow, + filters::{get_payment_filter_for_dimension, FilterRow}, + metrics::PaymentMetricRow, + PaymentMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + payments::{PaymentDistributionAccumulator, PaymentMetricAccumulator}, + AnalyticsProvider, +}; + +#[derive(Debug)] +pub enum TaskType { + MetricTask( + PaymentMetrics, + CustomResult, AnalyticsError>, + ), + DistributionTask( + PaymentDistributions, + CustomResult, AnalyticsError>, + ), +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetPaymentMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + PaymentMetricsBucketIdentifier, + PaymentMetricsAccumulator, + > = HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_metrics_query", + payment_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + TaskType::MetricTask(metric_type, data) + } + .instrument(task_span), + ); + } + + if let Some(distribution) = req.clone().distribution { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_distribution_query", + payment_distribution = distribution.distribution_for.as_ref() + ); + + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_distribution( + &distribution, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + TaskType::DistributionTask(distribution.distribution_for, data) + } + .instrument(task_span), + ); + } + + while let Some(task_type) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + match task_type { + TaskType::MetricTask(metric, data) => { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + PaymentMetrics::PaymentSuccessRate => metrics_builder + .payment_success_rate + .add_metrics_bucket(&value), + PaymentMetrics::PaymentCount => { + metrics_builder.payment_count.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + PaymentMetrics::AvgTicketSize => { + metrics_builder.avg_ticket_size.add_metrics_bucket(&value) + } + PaymentMetrics::RetriesCount => { + metrics_builder.retries_count.add_metrics_bucket(&value); + metrics_builder + .retries_amount_processed + .add_metrics_bucket(&value) + } + PaymentMetrics::ConnectorSuccessRate => { + metrics_builder + .connector_success_rate + .add_metrics_bucket(&value); + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + TaskType::DistributionTask(distribution, data) => { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("distribution_type", distribution.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for distribution {distribution}"); + let metrics_accumulator = metrics_accumulator.entry(id).or_default(); + match distribution { + PaymentDistributions::PaymentErrorMessage => metrics_accumulator + .payment_error_message + .add_distribution_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: distribution: {}, results: {:#?}", + distribution, + metrics_accumulator + ); + } + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetPaymentFiltersRequest, + merchant_id: &String, +) -> AnalyticsResult { + let mut res = PaymentFiltersResponse::default(); + + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_payment_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_payment_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres payments analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: FilterRow| match dim { + PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), + PaymentDimensions::Connector => fil.connector, + PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentMethod => fil.payment_method, + PaymentDimensions::PaymentMethodType => fil.payment_method_type, + }) + .collect::>(); + res.query_data.push(FilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/analytics/src/payments/distribution.rs b/crates/analytics/src/payments/distribution.rs new file mode 100644 index 000000000000..cf18c26310a7 --- /dev/null +++ b/crates/analytics/src/payments/distribution.rs @@ -0,0 +1,92 @@ +use api_models::analytics::{ + payments::{ + PaymentDimensions, PaymentDistributions, PaymentFilters, PaymentMetricsBucketIdentifier, + }, + Distribution, Granularity, TimeRange, +}; +use diesel_models::enums as storage_enums; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +mod payment_error_message; + +use payment_error_message::PaymentErrorMessage; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct PaymentDistributionRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub payment_method_type: Option, + pub total: Option, + pub count: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub end_bucket: Option, +} + +pub trait PaymentDistributionAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait PaymentDistribution +where + T: AnalyticsDataSource + PaymentDistributionAnalytics, +{ + #[allow(clippy::too_many_arguments)] + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl PaymentDistribution for PaymentDistributions +where + T: AnalyticsDataSource + PaymentDistributionAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentErrorMessage => { + PaymentErrorMessage + .load_distribution( + distribution, + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/payments/distribution/payment_error_message.rs b/crates/analytics/src/payments/distribution/payment_error_message.rs new file mode 100644 index 000000000000..c70fc09aeac4 --- /dev/null +++ b/crates/analytics/src/payments/distribution/payment_error_message.rs @@ -0,0 +1,176 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Distribution, Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::{PaymentDistribution, PaymentDistributionRow}; +use crate::{ + query::{ + Aggregate, GroupByClause, Order, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentErrorMessage; + +#[async_trait::async_trait] +impl PaymentDistribution for PaymentErrorMessage +where + T: AnalyticsDataSource + super::PaymentDistributionAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_distribution( + &self, + distribution: &Distribution, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(&distribution.distribution_for) + .switch()?; + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + query_builder + .add_group_by_clause(&distribution.distribution_for) + .attach_printable("Error grouping by distribution_for") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Failure, + ) + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_outer_select_column(dim).switch()?; + } + + query_builder + .add_outer_select_column(&distribution.distribution_for) + .switch()?; + query_builder.add_outer_select_column("count").switch()?; + query_builder + .add_outer_select_column("start_bucket") + .switch()?; + query_builder + .add_outer_select_column("end_bucket") + .switch()?; + let sql_dimensions = query_builder.transform_to_sql_values(dimensions).switch()?; + + query_builder + .add_outer_select_column(Window::Sum { + field: "count", + partition_by: Some(sql_dimensions), + order_by: None, + alias: Some("total"), + }) + .switch()?; + + query_builder + .add_top_n_clause( + dimensions, + distribution.distribution_cardinality.into(), + "count", + Order::Descending, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/filters.rs b/crates/analytics/src/payments/filters.rs similarity index 87% rename from crates/router/src/analytics/payments/filters.rs rename to crates/analytics/src/payments/filters.rs index f009aaa76329..6c165f78a8e4 100644 --- a/crates/router/src/analytics/payments/filters.rs +++ b/crates/analytics/src/payments/filters.rs @@ -1,11 +1,11 @@ use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{AttemptStatus, AuthenticationType, Currency}; use error_stack::ResultExt; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, LoadRow, @@ -26,6 +26,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); @@ -48,11 +49,12 @@ where .change_context(FiltersError::QueryExecutionFailure) } -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct FilterRow { pub currency: Option>, pub status: Option>, pub connector: Option, pub authentication_type: Option>, pub payment_method: Option, + pub payment_method_type: Option, } diff --git a/crates/router/src/analytics/payments/metrics.rs b/crates/analytics/src/payments/metrics.rs similarity index 76% rename from crates/router/src/analytics/payments/metrics.rs rename to crates/analytics/src/payments/metrics.rs index f492e5bd4df9..6fe6b6260d48 100644 --- a/crates/router/src/analytics/payments/metrics.rs +++ b/crates/analytics/src/payments/metrics.rs @@ -2,36 +2,44 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; mod avg_ticket_size; +mod connector_success_rate; mod payment_count; mod payment_processed_amount; mod payment_success_count; +mod retries_count; mod success_rate; use avg_ticket_size::AvgTicketSize; +use connector_success_rate::ConnectorSuccessRate; use payment_count::PaymentCount; use payment_processed_amount::PaymentProcessedAmount; use payment_success_count::PaymentSuccessCount; use success_rate::PaymentSuccessRate; -#[derive(Debug, PartialEq, Eq)] +use self::retries_count::RetriesCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] pub struct PaymentMetricRow { pub currency: Option>, pub status: Option>, pub connector: Option, pub authentication_type: Option>, pub payment_method: Option, + pub payment_method_type: Option, pub total: Option, pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub end_bucket: Option, } @@ -61,6 +69,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -132,6 +141,30 @@ where ) .await } + Self::RetriesCount => { + RetriesCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::ConnectorSuccessRate => { + ConnectorSuccessRate + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } } } } diff --git a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs b/crates/analytics/src/payments/metrics/avg_ticket_size.rs similarity index 90% rename from crates/router/src/analytics/payments/metrics/avg_ticket_size.rs rename to crates/analytics/src/payments/metrics/avg_ticket_size.rs index 2230d870e68a..9475d5288a64 100644 --- a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs +++ b/crates/analytics/src/payments/metrics/avg_ticket_size.rs @@ -3,12 +3,13 @@ use api_models::analytics::{ Granularity, TimeRange, }; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::{PaymentMetric, PaymentMetricRow}; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -89,6 +91,13 @@ where .switch()?; } + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + query_builder .execute_query::(pool) .await @@ -103,6 +112,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -119,7 +129,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/analytics/src/payments/metrics/connector_success_rate.rs b/crates/analytics/src/payments/metrics/connector_success_rate.rs new file mode 100644 index 000000000000..0c4d19b2e0ba --- /dev/null +++ b/crates/analytics/src/payments/metrics/connector_success_rate.rs @@ -0,0 +1,130 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ConnectorSuccessRate; + +#[async_trait::async_trait] +impl super::PaymentMetric for ConnectorSuccessRate +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(PaymentDimensions::PaymentStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_custom_filter_clause(PaymentDimensions::Connector, "NULL", FilterTypes::IsNotNull) + .switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_count.rs b/crates/analytics/src/payments/metrics/payment_count.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_count.rs rename to crates/analytics/src/payments/metrics/payment_count.rs index 661cec3dac36..34e71f3da6fb 100644 --- a/crates/router/src/analytics/payments/metrics/payment_count.rs +++ b/crates/analytics/src/payments/metrics/payment_count.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -97,6 +98,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -111,7 +113,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/payment_processed_amount.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_processed_amount.rs rename to crates/analytics/src/payments/metrics/payment_processed_amount.rs index 2ec0c6f18f9c..f2dbf97e0db9 100644 --- a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/payment_processed_amount.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -105,6 +106,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -121,7 +123,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/payments/metrics/payment_success_count.rs b/crates/analytics/src/payments/metrics/payment_success_count.rs similarity index 94% rename from crates/router/src/analytics/payments/metrics/payment_success_count.rs rename to crates/analytics/src/payments/metrics/payment_success_count.rs index 8245fe7aeb88..a6fb8ed2239d 100644 --- a/crates/router/src/analytics/payments/metrics/payment_success_count.rs +++ b/crates/analytics/src/payments/metrics/payment_success_count.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -104,6 +105,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -120,7 +122,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/analytics/src/payments/metrics/retries_count.rs b/crates/analytics/src/payments/metrics/retries_count.rs new file mode 100644 index 000000000000..91952adb569a --- /dev/null +++ b/crates/analytics/src/payments/metrics/retries_count.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RetriesCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for RetriesCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + _dimensions: &[PaymentDimensions], + merchant_id: &str, + _filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) + .switch()?; + query_builder + .add_custom_filter_clause("status", "succeeded", FilterTypes::Equal) + .switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + i.payment_method_type.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/success_rate.rs b/crates/analytics/src/payments/metrics/success_rate.rs similarity index 95% rename from crates/router/src/analytics/payments/metrics/success_rate.rs rename to crates/analytics/src/payments/metrics/success_rate.rs index c63956d4b157..9e688240ddbf 100644 --- a/crates/router/src/analytics/payments/metrics/success_rate.rs +++ b/crates/analytics/src/payments/metrics/success_rate.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -100,6 +101,7 @@ where i.connector.clone(), i.authentication_type.as_ref().map(|i| i.0), i.payment_method.clone(), + i.payment_method_type.clone(), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, @@ -116,7 +118,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/payments/types.rs b/crates/analytics/src/payments/types.rs similarity index 82% rename from crates/router/src/analytics/payments/types.rs rename to crates/analytics/src/payments/types.rs index fdfbedef383d..d5d8eca13e58 100644 --- a/crates/router/src/analytics/payments/types.rs +++ b/crates/analytics/src/payments/types.rs @@ -1,7 +1,7 @@ use api_models::analytics::payments::{PaymentDimensions, PaymentFilters}; use error_stack::ResultExt; -use crate::analytics::{ +use crate::{ query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, types::{AnalyticsCollection, AnalyticsDataSource}, }; @@ -41,6 +41,15 @@ where .add_filter_in_range_clause(PaymentDimensions::PaymentMethod, &self.payment_method) .attach_printable("Error adding payment method filter")?; } + + if !self.payment_method_type.is_empty() { + builder + .add_filter_in_range_clause( + PaymentDimensions::PaymentMethodType, + &self.payment_method_type, + ) + .attach_printable("Error adding payment method filter")?; + } Ok(()) } } diff --git a/crates/router/src/analytics/query.rs b/crates/analytics/src/query.rs similarity index 65% rename from crates/router/src/analytics/query.rs rename to crates/analytics/src/query.rs index b1f621d8153d..b924987f004c 100644 --- a/crates/router/src/analytics/query.rs +++ b/crates/analytics/src/query.rs @@ -1,26 +1,26 @@ -#![allow(dead_code)] use std::marker::PhantomData; use api_models::{ analytics::{ self as analytics_api, - payments::PaymentDimensions, + api_event::ApiEventDimensions, + payments::{PaymentDimensions, PaymentDistributions}, refunds::{RefundDimensions, RefundType}, + sdk_events::{SdkEventDimensions, SdkEventNames}, Granularity, }, - enums::Connector, + enums::{ + AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, + }, refunds::RefundStatus, }; -use common_enums::{ - enums as storage_enums, - enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}, -}; use common_utils::errors::{CustomResult, ParsingError}; +use diesel_models::enums as storage_enums; use error_stack::{IntoReport, ResultExt}; -use router_env::logger; +use router_env::{logger, Flow}; -use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow}; -use crate::analytics::types::QueryExecutionError; +use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, TableEngine}; +use crate::types::QueryExecutionError; pub type QueryResult = error_stack::Result; pub trait QueryFilter where @@ -89,12 +89,12 @@ impl GroupByClause for Granularity { let granularity_divisor = self.get_bucket_size(); builder - .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', modified_at)")) + .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', created_at)")) .attach_printable("Error adding time prune group by")?; if let Some(scale) = granularity_bucket_scale { builder .add_group_by_clause(format!( - "FLOOR(DATE_PART('{scale}', modified_at)/{granularity_divisor})" + "FLOOR(DATE_PART('{scale}', created_at)/{granularity_divisor})" )) .attach_printable("Error adding time binning group by")?; } @@ -102,6 +102,26 @@ impl GroupByClause for Granularity { } } +impl GroupByClause for Granularity { + fn set_group_by_clause( + &self, + builder: &mut QueryBuilder, + ) -> QueryResult<()> { + let interval = match self { + Self::OneMin => "toStartOfMinute(created_at)", + Self::FiveMin => "toStartOfFiveMinutes(created_at)", + Self::FifteenMin => "toStartOfFifteenMinutes(created_at)", + Self::ThirtyMin => "toStartOfInterval(created_at, INTERVAL 30 minute)", + Self::OneHour => "toStartOfHour(created_at)", + Self::OneDay => "toStartOfDay(created_at)", + }; + + builder + .add_group_by_clause(interval) + .attach_printable("Error adding interval group by") + } +} + #[derive(strum::Display)] #[strum(serialize_all = "lowercase")] pub enum TimeGranularityLevel { @@ -229,6 +249,76 @@ pub enum Aggregate { }, } +// Window functions in query +// --- +// Description - +// field: to_sql type value used as expr in aggregation +// partition_by: partition by fields in window +// order_by: order by fields and order (Ascending / Descending) in window +// alias: alias of window expr in query +// --- +// Usage - +// Window::Sum { +// field: "count", +// partition_by: Some(query_builder.transform_to_sql_values(&dimensions).switch()?), +// order_by: Some(("value", Descending)), +// alias: Some("total"), +// } +#[derive(Debug)] +pub enum Window { + Sum { + field: R, + partition_by: Option, + order_by: Option<(String, Order)>, + alias: Option<&'static str>, + }, + RowNumber { + field: R, + partition_by: Option, + order_by: Option<(String, Order)>, + alias: Option<&'static str>, + }, +} + +#[derive(Debug, Clone, Copy)] +pub enum Order { + Ascending, + Descending, +} + +impl ToString for Order { + fn to_string(&self) -> String { + String::from(match self { + Self::Ascending => "asc", + Self::Descending => "desc", + }) + } +} + +// Select TopN values for a group based on a metric +// --- +// Description - +// columns: Columns in group to select TopN values for +// count: N in TopN +// order_column: metric used to sort and limit TopN +// order: sort order of metric (Ascending / Descending) +// --- +// Usage - +// Use via add_top_n_clause fn of query_builder +// add_top_n_clause( +// &dimensions, +// distribution.distribution_cardinality.into(), +// "count", +// Order::Descending, +// ) +#[derive(Debug)] +pub struct TopN { + pub columns: String, + pub count: u64, + pub order_column: String, + pub order: Order, +} + #[derive(Debug)] pub struct QueryBuilder where @@ -239,13 +329,16 @@ where filters: Vec<(String, FilterTypes, String)>, group_by: Vec, having: Option>, + outer_select: Vec, + top_n: Option, table: AnalyticsCollection, distinct: bool, db_type: PhantomData, + table_engine: TableEngine, } pub trait ToSql { - fn to_sql(&self) -> error_stack::Result; + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result; } /// Implement `ToSql` on arrays of types that impl `ToString`. @@ -253,7 +346,7 @@ macro_rules! impl_to_sql_for_to_string { ($($type:ty),+) => { $( impl ToSql for $type { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { Ok(self.to_string()) } } @@ -267,8 +360,10 @@ impl_to_sql_for_to_string!( &PaymentDimensions, &RefundDimensions, PaymentDimensions, + &PaymentDistributions, RefundDimensions, PaymentMethod, + PaymentMethodType, AuthenticationType, Connector, AttemptStatus, @@ -276,12 +371,18 @@ impl_to_sql_for_to_string!( storage_enums::RefundStatus, Currency, RefundType, + Flow, &String, &bool, - &u64 + &u64, + u64, + Order ); -#[allow(dead_code)] +impl_to_sql_for_to_string!(&SdkEventDimensions, SdkEventDimensions, SdkEventNames); + +impl_to_sql_for_to_string!(&ApiEventDimensions, ApiEventDimensions); + #[derive(Debug)] pub enum FilterTypes { Equal, @@ -290,6 +391,23 @@ pub enum FilterTypes { Gte, Lte, Gt, + Like, + NotLike, + IsNotNull, +} + +pub fn filter_type_to_sql(l: &String, op: &FilterTypes, r: &String) -> String { + match op { + FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::Equal => format!("{l} = '{r}'"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= '{r}'"), + FilterTypes::Gt => format!("{l} > {r}"), + FilterTypes::Lte => format!("{l} <= '{r}'"), + FilterTypes::Like => format!("{l} LIKE '%{r}%'"), + FilterTypes::NotLike => format!("{l} NOT LIKE '%{r}%'"), + FilterTypes::IsNotNull => format!("{l} IS NOT NULL"), + } } impl QueryBuilder @@ -303,22 +421,68 @@ where filters: Default::default(), group_by: Default::default(), having: Default::default(), + outer_select: Default::default(), + top_n: Default::default(), table, distinct: Default::default(), db_type: Default::default(), + table_engine: T::get_table_engine(table), } } pub fn add_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { self.columns.push( column - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing select column")?, ); Ok(()) } + pub fn transform_to_sql_values(&mut self, values: &[impl ToSql]) -> QueryResult { + let res = values + .iter() + .map(|i| i.to_sql(&self.table_engine)) + .collect::, ParsingError>>() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing range filter value")? + .join(", "); + Ok(res) + } + + pub fn add_top_n_clause( + &mut self, + columns: &[impl ToSql], + count: u64, + order_column: impl ToSql, + order: Order, + ) -> QueryResult<()> + where + Window<&'static str>: ToSql, + { + let partition_by_columns = self.transform_to_sql_values(columns)?; + let order_by_column = order_column + .to_sql(&self.table_engine) + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing select column")?; + + self.add_outer_select_column(Window::RowNumber { + field: "", + partition_by: Some(partition_by_columns.clone()), + order_by: Some((order_by_column.clone(), order)), + alias: Some("top_n"), + })?; + + self.top_n = Some(TopN { + columns: partition_by_columns, + count, + order_column: order_by_column, + order, + }); + Ok(()) + } + pub fn set_distinct(&mut self) { self.distinct = true } @@ -346,11 +510,11 @@ where comparison: FilterTypes, ) -> QueryResult<()> { self.filters.push(( - lhs.to_sql() + lhs.to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing filter key")?, comparison, - rhs.to_sql() + rhs.to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing filter value")?, )); @@ -366,7 +530,7 @@ where .iter() .map(|i| { // trimming whitespaces from the filter values received in request, to prevent a possibility of an SQL injection - i.to_sql().map(|s| { + i.to_sql(&self.table_engine).map(|s| { let trimmed_str = s.replace(' ', ""); format!("'{trimmed_str}'") }) @@ -381,7 +545,7 @@ where pub fn add_group_by_clause(&mut self, column: impl ToSql) -> QueryResult<()> { self.group_by.push( column - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing group by field")?, ); @@ -406,14 +570,7 @@ where fn get_filter_clause(&self) -> String { self.filters .iter() - .map(|(l, op, r)| match op { - FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::Equal => format!("{l} = '{r}'"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= '{r}'"), - FilterTypes::Gt => format!("{l} > {r}"), - FilterTypes::Lte => format!("{l} <= '{r}'"), - }) + .map(|(l, op, r)| filter_type_to_sql(l, op, r)) .collect::>() .join(" AND ") } @@ -426,7 +583,10 @@ where self.group_by.join(", ") } - #[allow(dead_code)] + fn get_outer_select_clause(&self) -> String { + self.outer_select.join(", ") + } + pub fn add_having_clause( &mut self, aggregate: Aggregate, @@ -437,11 +597,11 @@ where Aggregate: ToSql, { let aggregate = aggregate - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing having aggregate")?; let value = value - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing having value")?; let entry = (aggregate, filter_type, value); @@ -453,16 +613,20 @@ where Ok(()) } + pub fn add_outer_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { + self.outer_select.push( + column + .to_sql(&self.table_engine) + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing outer select column")?, + ); + Ok(()) + } + pub fn get_filter_type_clause(&self) -> Option { self.having.as_ref().map(|vec| { vec.iter() - .map(|(l, op, r)| match op { - FilterTypes::Equal | FilterTypes::EqualBool => format!("{l} = {r}"), - FilterTypes::In => format!("{l} IN ({r})"), - FilterTypes::Gte => format!("{l} >= {r}"), - FilterTypes::Lte => format!("{l} < {r}"), - FilterTypes::Gt => format!("{l} > {r}"), - }) + .map(|(l, op, r)| filter_type_to_sql(l, op, r)) .collect::>() .join(" AND ") }) @@ -471,6 +635,7 @@ where pub fn build_query(&mut self) -> QueryResult where Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { if self.columns.is_empty() { Err(QueryBuildingError::InvalidQuery( @@ -491,7 +656,7 @@ where query.push_str( &self .table - .to_sql() + .to_sql(&self.table_engine) .change_context(QueryBuildingError::SqlSerializeError) .attach_printable("Error serializing table value")?, ); @@ -504,6 +669,16 @@ where if !self.group_by.is_empty() { query.push_str(" GROUP BY "); query.push_str(&self.get_group_by_clause()); + if let TableEngine::CollapsingMergeTree { sign } = self.table_engine { + self.add_having_clause( + Aggregate::Count { + field: Some(sign), + alias: None, + }, + FilterTypes::Gte, + "1", + )?; + } } if self.having.is_some() { @@ -512,6 +687,22 @@ where query.push_str(condition.as_str()); } } + + if !self.outer_select.is_empty() { + query.insert_str( + 0, + format!("SELECT {} FROM (", &self.get_outer_select_clause()).as_str(), + ); + query.push_str(") _"); + } + + if let Some(top_n) = &self.top_n { + query.insert_str(0, "SELECT * FROM ("); + query.push_str(format!(") _ WHERE top_n <= {}", top_n.count).as_str()); + } + + println!("{}", query); + Ok(query) } @@ -522,6 +713,7 @@ where where P: LoadRow, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let query = self .build_query() diff --git a/crates/router/src/analytics/refunds.rs b/crates/analytics/src/refunds.rs similarity index 81% rename from crates/router/src/analytics/refunds.rs rename to crates/analytics/src/refunds.rs index a8b52effe76d..53481e232817 100644 --- a/crates/router/src/analytics/refunds.rs +++ b/crates/analytics/src/refunds.rs @@ -7,4 +7,4 @@ pub mod types; pub use accumulator::{RefundMetricAccumulator, RefundMetricsAccumulator}; pub trait RefundAnalytics: metrics::RefundMetricAnalytics {} -pub use self::core::get_metrics; +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/router/src/analytics/refunds/accumulator.rs b/crates/analytics/src/refunds/accumulator.rs similarity index 98% rename from crates/router/src/analytics/refunds/accumulator.rs rename to crates/analytics/src/refunds/accumulator.rs index 3d0c0e659f6c..9c51defdcf91 100644 --- a/crates/router/src/analytics/refunds/accumulator.rs +++ b/crates/analytics/src/refunds/accumulator.rs @@ -1,5 +1,5 @@ use api_models::analytics::refunds::RefundMetricsBucketValue; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use super::metrics::RefundMetricRow; #[derive(Debug, Default)] @@ -15,13 +15,11 @@ pub struct SuccessRateAccumulator { pub success: i64, pub total: i64, } - #[derive(Debug, Default)] #[repr(transparent)] pub struct CountAccumulator { pub count: Option, } - #[derive(Debug, Default)] #[repr(transparent)] pub struct SumAccumulator { diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs new file mode 100644 index 000000000000..25a1e228f567 --- /dev/null +++ b/crates/analytics/src/refunds/core.rs @@ -0,0 +1,203 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + refunds::{ + RefundDimensions, RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse, + }, + AnalyticsMetadata, GetRefundFilterRequest, GetRefundMetricRequest, MetricsResponse, + RefundFilterValue, RefundFiltersResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, Instrument}, +}; + +use super::{ + filters::{get_refund_filter_for_dimension, RefundFilterRow}, + RefundMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + refunds::RefundMetricAccumulator, + AnalyticsProvider, +}; + +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &String, + req: GetRefundMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_refund_query", + refund_metric = metric_type.as_ref() + ); + // Currently JoinSet works with only static lifetime references even if the task pool does not outlive the given reference + // We can optimize away this clone once that is fixed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_refund_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes("source", pool.to_string()), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + RefundMetrics::RefundSuccessRate => metrics_builder + .refund_success_rate + .add_metrics_bucket(&value), + RefundMetrics::RefundCount => { + metrics_builder.refund_count.add_metrics_bucket(&value) + } + RefundMetrics::RefundSuccessCount => { + metrics_builder.refund_success.add_metrics_bucket(&value) + } + RefundMetrics::RefundProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| RefundMetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) +} + +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetRefundFilterRequest, + merchant_id: &String, +) -> AnalyticsResult { + let mut res = RefundFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_refund_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_refund_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) => { + let ckh_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_pool, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => { + let ckh_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_refund_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_pool, + ) + .await; + match (&sqlx_result, &ckh_result) { + (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => { + router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: RefundFilterRow| match dim { + RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), + RefundDimensions::Connector => fil.connector, + RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), + }) + .collect::>(); + res.query_data.push(RefundFilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/router/src/analytics/refunds/filters.rs b/crates/analytics/src/refunds/filters.rs similarity index 90% rename from crates/router/src/analytics/refunds/filters.rs rename to crates/analytics/src/refunds/filters.rs index 6b45e9194fad..29375483eb9a 100644 --- a/crates/router/src/analytics/refunds/filters.rs +++ b/crates/analytics/src/refunds/filters.rs @@ -2,13 +2,13 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundType}, Granularity, TimeRange, }; -use common_enums::enums::{Currency, RefundStatus}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{Currency, RefundStatus}; use error_stack::ResultExt; use time::PrimitiveDateTime; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, LoadRow, @@ -28,6 +28,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); @@ -49,8 +50,7 @@ where .change_context(FiltersError::QueryBuildingError)? .change_context(FiltersError::QueryExecutionFailure) } - -#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] pub struct RefundFilterRow { pub currency: Option>, pub refund_status: Option>, diff --git a/crates/router/src/analytics/refunds/metrics.rs b/crates/analytics/src/refunds/metrics.rs similarity index 91% rename from crates/router/src/analytics/refunds/metrics.rs rename to crates/analytics/src/refunds/metrics.rs index d4f509b4a1e3..10cd03546772 100644 --- a/crates/router/src/analytics/refunds/metrics.rs +++ b/crates/analytics/src/refunds/metrics.rs @@ -4,7 +4,7 @@ use api_models::analytics::{ }, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; +use diesel_models::enums as storage_enums; use time::PrimitiveDateTime; mod refund_count; mod refund_processed_amount; @@ -15,12 +15,11 @@ use refund_processed_amount::RefundProcessedAmount; use refund_success_count::RefundSuccessCount; use refund_success_rate::RefundSuccessRate; -use crate::analytics::{ - query::{Aggregate, GroupByClause, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; - -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, serde::Deserialize)] pub struct RefundMetricRow { pub currency: Option>, pub refund_status: Option>, @@ -28,7 +27,9 @@ pub struct RefundMetricRow { pub refund_type: Option>, pub total: Option, pub count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub end_bucket: Option, } @@ -42,6 +43,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -62,6 +64,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, diff --git a/crates/router/src/analytics/refunds/metrics/refund_count.rs b/crates/analytics/src/refunds/metrics/refund_count.rs similarity index 94% rename from crates/router/src/analytics/refunds/metrics/refund_count.rs rename to crates/analytics/src/refunds/metrics/refund_count.rs index 471327235073..cf3c7a509278 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_count.rs +++ b/crates/analytics/src/refunds/metrics/refund_count.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -93,7 +94,7 @@ where Ok(( RefundMetricsBucketIdentifier::new( i.currency.as_ref().map(|i| i.0), - i.refund_status.as_ref().map(|i| i.0), + i.refund_status.as_ref().map(|i| i.0.to_string()), i.connector.clone(), i.refund_type.as_ref().map(|i| i.0.to_string()), TimeRange { @@ -110,7 +111,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs similarity index 95% rename from crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs rename to crates/analytics/src/refunds/metrics/refund_processed_amount.rs index c5f3a706aaef..661fca57b282 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs +++ b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] @@ -23,6 +23,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -116,7 +117,7 @@ where i, )) }) - .collect::, crate::analytics::query::PostProcessingError>>() + .collect::, crate::query::PostProcessingError>>() .change_context(MetricsError::PostProcessingFailure) } } diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs b/crates/analytics/src/refunds/metrics/refund_success_count.rs similarity index 95% rename from crates/router/src/analytics/refunds/metrics/refund_success_count.rs rename to crates/analytics/src/refunds/metrics/refund_success_count.rs index 0c8032908fd7..bc09d8b7ab64 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs +++ b/crates/analytics/src/refunds/metrics/refund_success_count.rs @@ -2,14 +2,14 @@ use api_models::analytics::{ refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, Granularity, TimeRange, }; -use common_enums::enums as storage_enums; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -24,6 +24,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -115,7 +116,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs b/crates/analytics/src/refunds/metrics/refund_success_rate.rs similarity index 96% rename from crates/router/src/analytics/refunds/metrics/refund_success_rate.rs rename to crates/analytics/src/refunds/metrics/refund_success_rate.rs index 42f9ccf8d3c0..29b73b885d8e 100644 --- a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs +++ b/crates/analytics/src/refunds/metrics/refund_success_rate.rs @@ -7,8 +7,8 @@ use error_stack::ResultExt; use time::PrimitiveDateTime; use super::RefundMetricRow; -use crate::analytics::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] @@ -22,6 +22,7 @@ where AnalyticsCollection: ToSql, Granularity: GroupByClause, Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, { async fn load_metrics( &self, @@ -110,7 +111,7 @@ where }) .collect::, - crate::analytics::query::PostProcessingError, + crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) } diff --git a/crates/router/src/analytics/refunds/types.rs b/crates/analytics/src/refunds/types.rs similarity index 98% rename from crates/router/src/analytics/refunds/types.rs rename to crates/analytics/src/refunds/types.rs index fbfd69972671..d7d739e1aba7 100644 --- a/crates/router/src/analytics/refunds/types.rs +++ b/crates/analytics/src/refunds/types.rs @@ -1,7 +1,7 @@ use api_models::analytics::refunds::{RefundDimensions, RefundFilters}; use error_stack::ResultExt; -use crate::analytics::{ +use crate::{ query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, types::{AnalyticsCollection, AnalyticsDataSource}, }; diff --git a/crates/analytics/src/sdk_events.rs b/crates/analytics/src/sdk_events.rs new file mode 100644 index 000000000000..fe8af7cfe2df --- /dev/null +++ b/crates/analytics/src/sdk_events.rs @@ -0,0 +1,14 @@ +pub mod accumulator; +mod core; +pub mod events; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{SdkEventMetricAccumulator, SdkEventMetricsAccumulator}; +pub trait SDKEventAnalytics: events::SdkEventsFilterAnalytics {} +pub trait SdkEventAnalytics: + metrics::SdkEventMetricAnalytics + filters::SdkEventFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics, sdk_events_core}; diff --git a/crates/analytics/src/sdk_events/accumulator.rs b/crates/analytics/src/sdk_events/accumulator.rs new file mode 100644 index 000000000000..ab9e9309434f --- /dev/null +++ b/crates/analytics/src/sdk_events/accumulator.rs @@ -0,0 +1,98 @@ +use api_models::analytics::sdk_events::SdkEventMetricsBucketValue; +use router_env::logger; + +use super::metrics::SdkEventMetricRow; + +#[derive(Debug, Default)] +pub struct SdkEventMetricsAccumulator { + pub payment_attempts: CountAccumulator, + pub payment_success: CountAccumulator, + pub payment_methods_call_count: CountAccumulator, + pub average_payment_time: AverageAccumulator, + pub sdk_initiated_count: CountAccumulator, + pub sdk_rendered_count: CountAccumulator, + pub payment_method_selected_count: CountAccumulator, + pub payment_data_filled_count: CountAccumulator, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +pub struct AverageAccumulator { + pub total: u32, + pub count: u32, +} + +pub trait SdkEventMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl SdkEventMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl SdkEventMetricAccumulator for AverageAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &SdkEventMetricRow) { + let total = metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_u32); + let count = metrics.count.and_then(|total| u32::try_from(total).ok()); + + match (total, count) { + (Some(total), Some(count)) => { + self.total += total; + self.count += count; + } + _ => { + logger::error!(message="Dropping metrics for average accumulator", metric=?metrics); + } + } + } + + fn collect(self) -> Self::MetricOutput { + if self.count == 0 { + None + } else { + Some(f64::from(self.total) / f64::from(self.count)) + } + } +} + +impl SdkEventMetricsAccumulator { + #[allow(dead_code)] + pub fn collect(self) -> SdkEventMetricsBucketValue { + SdkEventMetricsBucketValue { + payment_attempts: self.payment_attempts.collect(), + payment_success_count: self.payment_success.collect(), + payment_methods_call_count: self.payment_methods_call_count.collect(), + average_payment_time: self.average_payment_time.collect(), + sdk_initiated_count: self.sdk_initiated_count.collect(), + sdk_rendered_count: self.sdk_rendered_count.collect(), + payment_method_selected_count: self.payment_method_selected_count.collect(), + payment_data_filled_count: self.payment_data_filled_count.collect(), + } + } +} diff --git a/crates/analytics/src/sdk_events/core.rs b/crates/analytics/src/sdk_events/core.rs new file mode 100644 index 000000000000..34f23c745b05 --- /dev/null +++ b/crates/analytics/src/sdk_events/core.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + sdk_events::{ + MetricsBucketResponse, SdkEventMetrics, SdkEventMetricsBucketIdentifier, SdkEventsRequest, + }, + AnalyticsMetadata, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, MetricsResponse, + SdkEventFiltersResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{instrument, logger, tracing}; + +use super::{ + events::{get_sdk_event, SdkEventsResult}, + SdkEventMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + sdk_events::SdkEventMetricAccumulator, + types::FiltersError, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn sdk_events_core( + pool: &AnalyticsProvider, + req: SdkEventsRequest, + publishable_key: String, +) -> AnalyticsResult> { + match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for Sdk Events"), + AnalyticsProvider::Clickhouse(pool) => get_sdk_event(&publishable_key, req, pool).await, + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event(&publishable_key, req, ckh_pool).await + } + } + .change_context(AnalyticsError::UnknownError) +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + publishable_key: Option<&String>, + req: GetSdkEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + SdkEventMetricsBucketIdentifier, + SdkEventMetricsAccumulator, + > = HashMap::new(); + + if let Some(publishable_key) = publishable_key { + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let publishable_key_scoped = publishable_key.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_sdk_event_metrics( + &metric_type, + &req.group_by_names.clone(), + &publishable_key_scoped, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + logger::info!("Logging Result {:?}", data); + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + SdkEventMetrics::PaymentAttempts => { + metrics_builder.payment_attempts.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + SdkEventMetrics::PaymentMethodsCallCount => metrics_builder + .payment_methods_call_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkRenderedCount => metrics_builder + .sdk_rendered_count + .add_metrics_bucket(&value), + SdkEventMetrics::SdkInitiatedCount => metrics_builder + .sdk_initiated_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentMethodSelectedCount => metrics_builder + .payment_method_selected_count + .add_metrics_bucket(&value), + SdkEventMetrics::PaymentDataFilledCount => metrics_builder + .payment_data_filled_count + .add_metrics_bucket(&value), + SdkEventMetrics::AveragePaymentTime => metrics_builder + .average_payment_time + .add_metrics_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } else { + logger::error!("Publishable key not present for merchant ID"); + Ok(MetricsResponse { + query_data: vec![], + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } +} + +#[allow(dead_code)] +pub async fn get_filters( + pool: &AnalyticsProvider, + req: GetSdkEventFiltersRequest, + publishable_key: Option<&String>, +) -> AnalyticsResult { + use api_models::analytics::{sdk_events::SdkEventDimensions, SdkEventFilterValue}; + + use super::filters::get_sdk_event_filter_for_dimension; + use crate::sdk_events::filters::SdkEventFilter; + + let mut res = SdkEventFiltersResponse::default(); + + if let Some(publishable_key) = publishable_key { + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) + .into_report() + .attach_printable("SQL Analytics is not implemented for SDK Events"), + AnalyticsProvider::Clickhouse(pool) => { + get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) + | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { + get_sdk_event_filter_for_dimension( + dim, + publishable_key, + &req.time_range, + ckh_pool, + ) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: SdkEventFilter| match dim { + SdkEventDimensions::PaymentMethod => fil.payment_method, + SdkEventDimensions::Platform => fil.platform, + SdkEventDimensions::BrowserName => fil.browser_name, + SdkEventDimensions::Source => fil.source, + SdkEventDimensions::Component => fil.component, + SdkEventDimensions::PaymentExperience => fil.payment_experience, + }) + .collect::>(); + res.query_data.push(SdkEventFilterValue { + dimension: dim, + values, + }) + } + } else { + router_env::logger::error!("Publishable key not found for merchant"); + } + + Ok(res) +} diff --git a/crates/analytics/src/sdk_events/events.rs b/crates/analytics/src/sdk_events/events.rs new file mode 100644 index 000000000000..a4d044267e94 --- /dev/null +++ b/crates/analytics/src/sdk_events/events.rs @@ -0,0 +1,80 @@ +use api_models::analytics::{ + sdk_events::{SdkEventNames, SdkEventsRequest}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use strum::IntoEnumIterator; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait SdkEventsFilterAnalytics: LoadRow {} + +pub async fn get_sdk_event( + merchant_id: &str, + request: SdkEventsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + SdkEventsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let static_event_list = SdkEventNames::iter() + .map(|i| format!("'{}'", i.as_ref())) + .collect::>() + .join(","); + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_filter_clause("payment_id", request.payment_id) + .switch()?; + query_builder + .add_custom_filter_clause("event_name", static_event_list, FilterTypes::In) + .switch()?; + let _ = &request + .time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SdkEventsResult { + pub merchant_id: String, + pub payment_id: String, + pub event_name: Option, + pub log_type: Option, + pub first_event: bool, + pub browser_name: Option, + pub browser_version: Option, + pub source: Option, + pub category: Option, + pub version: Option, + pub value: Option, + pub platform: Option, + pub component: Option, + pub payment_method: Option, + pub payment_experience: Option, + pub latency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at_precise: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/sdk_events/filters.rs b/crates/analytics/src/sdk_events/filters.rs new file mode 100644 index 000000000000..9963f51ef947 --- /dev/null +++ b/crates/analytics/src/sdk_events/filters.rs @@ -0,0 +1,56 @@ +use api_models::analytics::{sdk_events::SdkEventDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; + +pub trait SdkEventFilterAnalytics: LoadRow {} + +pub async fn get_sdk_event_filter_for_dimension( + dimension: SdkEventDimensions, + publishable_key: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + SdkEventFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct SdkEventFilter { + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, +} diff --git a/crates/analytics/src/sdk_events/metrics.rs b/crates/analytics/src/sdk_events/metrics.rs new file mode 100644 index 000000000000..354d2270d18a --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics.rs @@ -0,0 +1,181 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod average_payment_time; +mod payment_attempts; +mod payment_data_filled_count; +mod payment_method_selected_count; +mod payment_methods_call_count; +mod payment_success_count; +mod sdk_initiated_count; +mod sdk_rendered_count; + +use average_payment_time::AveragePaymentTime; +use payment_attempts::PaymentAttempts; +use payment_data_filled_count::PaymentDataFilledCount; +use payment_method_selected_count::PaymentMethodSelectedCount; +use payment_methods_call_count::PaymentMethodsCallCount; +use payment_success_count::PaymentSuccessCount; +use sdk_initiated_count::SdkInitiatedCount; +use sdk_rendered_count::SdkRenderedCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct SdkEventMetricRow { + pub total: Option, + pub count: Option, + pub time_bucket: Option, + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, +} + +pub trait SdkEventMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait SdkEventMetric +where + T: AnalyticsDataSource + SdkEventMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl SdkEventMetric for SdkEventMetrics +where + T: AnalyticsDataSource + SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentAttempts => { + PaymentAttempts + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentSuccessCount => { + PaymentSuccessCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentMethodsCallCount => { + PaymentMethodsCallCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SdkRenderedCount => { + SdkRenderedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SdkInitiatedCount => { + SdkInitiatedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentMethodSelectedCount => { + PaymentMethodSelectedCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentDataFilledCount => { + PaymentDataFilledCount + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::AveragePaymentTime => { + AveragePaymentTime + .load_metrics( + dimensions, + publishable_key, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/sdk_events/metrics/average_payment_time.rs b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs new file mode 100644 index 000000000000..db7171524ae5 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs @@ -0,0 +1,129 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AveragePaymentTime; + +#[async_trait::async_trait] +impl super::SdkEventMetric for AveragePaymentTime +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Sum { + field: "latency", + alias: Some("total"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentAttempt) + .switch()?; + + query_builder + .add_custom_filter_clause("latency", 0, FilterTypes::Gt) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_attempts.rs b/crates/analytics/src/sdk_events/metrics/payment_attempts.rs new file mode 100644 index 000000000000..b2a78188c4f2 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_attempts.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentAttempts; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentAttempts +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentAttempt) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs b/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs new file mode 100644 index 000000000000..a3c94baeda26 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_data_filled_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentDataFilledCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentDataFilledCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentDataFilled) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs b/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs new file mode 100644 index 000000000000..11aeac5e6ff9 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_method_selected_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentMethodSelectedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentMethodSelectedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentMethodChanged) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs b/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs new file mode 100644 index 000000000000..7570f1292e5e --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_methods_call_count.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentMethodsCallCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentMethodsCallCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentMethodsCall) + .switch()?; + + query_builder + .add_filter_clause("log_type", "INFO") + .switch()?; + + query_builder + .add_filter_clause("category", "API") + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/payment_success_count.rs b/crates/analytics/src/sdk_events/metrics/payment_success_count.rs new file mode 100644 index 000000000000..3faf8213632f --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/payment_success_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for PaymentSuccessCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::PaymentSuccess) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs b/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs new file mode 100644 index 000000000000..a525e7890b75 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/sdk_initiated_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SdkInitiatedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for SdkInitiatedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::StripeElementsCalled) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs b/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs new file mode 100644 index 000000000000..ed9e776423a8 --- /dev/null +++ b/crates/analytics/src/sdk_events/metrics/sdk_rendered_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + sdk_events::{ + SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::SdkEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SdkRenderedCount; + +#[async_trait::async_trait] +impl super::SdkEventMetric for SdkRenderedCount +where + T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[SdkEventDimensions], + publishable_key: &str, + filters: &SdkEventFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); + let dimensions = dimensions.to_vec(); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", publishable_key) + .switch()?; + + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::AppRendered) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + SdkEventMetricsBucketIdentifier::new( + i.payment_method.clone(), + i.platform.clone(), + i.browser_name.clone(), + i.source.clone(), + i.component.clone(), + i.payment_experience.clone(), + i.time_bucket.clone(), + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/types.rs b/crates/analytics/src/sdk_events/types.rs new file mode 100644 index 000000000000..d631b3158ed4 --- /dev/null +++ b/crates/analytics/src/sdk_events/types.rs @@ -0,0 +1,50 @@ +use api_models::analytics::sdk_events::{SdkEventDimensions, SdkEventFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for SdkEventFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.payment_method.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::PaymentMethod, &self.payment_method) + .attach_printable("Error adding payment method filter")?; + } + if !self.platform.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Platform, &self.platform) + .attach_printable("Error adding platform filter")?; + } + if !self.browser_name.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::BrowserName, &self.browser_name) + .attach_printable("Error adding browser name filter")?; + } + if !self.source.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Source, &self.source) + .attach_printable("Error adding source filter")?; + } + if !self.component.is_empty() { + builder + .add_filter_in_range_clause(SdkEventDimensions::Component, &self.component) + .attach_printable("Error adding component filter")?; + } + if !self.payment_experience.is_empty() { + builder + .add_filter_in_range_clause( + SdkEventDimensions::PaymentExperience, + &self.payment_experience, + ) + .attach_printable("Error adding payment experience filter")?; + } + Ok(()) + } +} diff --git a/crates/router/src/analytics/sqlx.rs b/crates/analytics/src/sqlx.rs similarity index 64% rename from crates/router/src/analytics/sqlx.rs rename to crates/analytics/src/sqlx.rs index b88a2065f0b0..cdd2647e4e71 100644 --- a/crates/router/src/analytics/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -1,14 +1,11 @@ use std::{fmt::Display, str::FromStr}; use api_models::analytics::refunds::RefundType; -use common_enums::enums::{ +use common_utils::errors::{CustomResult, ParsingError}; +use diesel_models::enums::{ AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, }; -use common_utils::errors::{CustomResult, ParsingError}; use error_stack::{IntoReport, ResultExt}; -#[cfg(feature = "kms")] -use external_services::{kms, kms::decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] use masking::PeekInterface; use sqlx::{ postgres::{PgArgumentBuffer, PgPoolOptions, PgRow, PgTypeInfo, PgValueRef}, @@ -16,15 +13,16 @@ use sqlx::{ Error::ColumnNotFound, FromRow, Pool, Postgres, Row, }; +use storage_impl::config::Database; use time::PrimitiveDateTime; use super::{ - query::{Aggregate, ToSql}, + query::{Aggregate, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, + TableEngine, }, }; -use crate::configs::settings::Database; #[derive(Debug, Clone)] pub struct SqlxClient { @@ -47,19 +45,7 @@ impl Default for SqlxClient { } impl SqlxClient { - pub async fn from_conf( - conf: &Database, - #[cfg(feature = "kms")] kms_client: &kms::KmsClient, - ) -> Self { - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let password = conf - .password - .decrypt_inner(kms_client) - .await - .expect("Failed to KMS decrypt database password"); - - #[cfg(not(feature = "kms"))] + pub async fn from_conf(conf: &Database) -> Self { let password = &conf.password.peek(); let database_url = format!( "postgres://{}:{}@{}:{}/{}", @@ -154,6 +140,7 @@ where impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} +impl super::payments::distribution::PaymentDistributionAnalytics for SqlxClient {} impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} @@ -207,7 +194,7 @@ impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; - + // Removing millisecond precision to get accurate diffs against clickhouse let start_bucket: Option = row .try_get::, _>("start_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); @@ -253,6 +240,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let total: Option = row.try_get("total").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -261,7 +253,72 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + // Removing millisecond precision to get accurate diffs against clickhouse + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + payment_method_type, + total, + count, + start_bucket, + end_bucket, + }) + } +} +impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributionRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let error_message: Option = row.try_get("error_message").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + // Removing millisecond precision to get accurate diffs against clickhouse let start_bucket: Option = row .try_get::, _>("start_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); @@ -274,8 +331,10 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { connector, authentication_type, payment_method, + payment_method_type, total, count, + error_message, start_bucket, end_bucket, }) @@ -308,12 +367,18 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let payment_method_type: Option = + row.try_get("payment_method_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; Ok(Self { currency, status, connector, authentication_type, payment_method, + payment_method_type, }) } } @@ -349,16 +414,21 @@ impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { } impl ToSql for PrimitiveDateTime { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { Ok(self.to_string()) } } impl ToSql for AnalyticsCollection { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { match self { Self::Payment => Ok("payment_attempt".to_string()), Self::Refund => Ok("refund".to_string()), + Self::SdkEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("SdkEvents table is not implemented for Sqlx"))?, + Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("ApiEvents table is not implemented for Sqlx"))?, + Self::PaymentIntent => Ok("payment_intent".to_string()), } } } @@ -367,7 +437,7 @@ impl ToSql for Aggregate where T: ToSql, { - fn to_sql(&self) -> error_stack::Result { + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { Ok(match self { Self::Count { field: _, alias } => { format!( @@ -378,21 +448,86 @@ where Self::Sum { field, alias } => { format!( "sum({}){}", - field.to_sql().attach_printable("Failed to sum aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to sum aggregate")?, alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } Self::Min { field, alias } => { format!( "min({}){}", - field.to_sql().attach_printable("Failed to min aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to min aggregate")?, alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } Self::Max { field, alias } => { format!( "max({}){}", - field.to_sql().attach_printable("Failed to max aggregate")?, + field + .to_sql(table_engine) + .attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} + +impl ToSql for Window +where + T: ToSql, +{ + fn to_sql(&self, table_engine: &TableEngine) -> error_stack::Result { + Ok(match self { + Self::Sum { + field, + partition_by, + order_by, + alias, + } => { + format!( + "sum({}) over ({}{}){}", + field + .to_sql(table_engine) + .attach_printable("Failed to sum window")?, + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::RowNumber { + field: _, + partition_by, + order_by, + alias, + } => { + format!( + "row_number() over ({}{}){}", + partition_by.as_ref().map_or_else( + || "".to_owned(), + |partition_by| format!("partition by {}", partition_by.to_owned()) + ), + order_by.as_ref().map_or_else( + || "".to_owned(), + |(order_column, order)| format!( + " order by {} {}", + order_column.to_owned(), + order.to_string() + ) + ), alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } diff --git a/crates/router/src/analytics/types.rs b/crates/analytics/src/types.rs similarity index 83% rename from crates/router/src/analytics/types.rs rename to crates/analytics/src/types.rs index fe20e812a9b8..16d342d3d2ee 100644 --- a/crates/router/src/analytics/types.rs +++ b/crates/analytics/src/types.rs @@ -2,25 +2,36 @@ use std::{fmt::Display, str::FromStr}; use common_utils::{ errors::{CustomResult, ErrorSwitch, ParsingError}, - events::ApiEventMetric, + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, }; use error_stack::{report, Report, ResultExt}; use super::query::QueryBuildingError; -#[derive(serde::Deserialize, Debug, masking::Serialize)] +#[derive(serde::Deserialize, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum AnalyticsDomain { Payments, Refunds, + SdkEvents, + ApiEvents, } -impl ApiEventMetric for AnalyticsDomain {} - #[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] pub enum AnalyticsCollection { Payment, Refund, + SdkEvents, + ApiEvents, + PaymentIntent, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum TableEngine { + CollapsingMergeTree { sign: &'static str }, + BasicTree, } #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] @@ -50,6 +61,7 @@ where // Analytics Framework pub trait RefundAnalytics {} +pub trait SdkEventAnalytics {} #[async_trait::async_trait] pub trait AnalyticsDataSource @@ -60,6 +72,10 @@ where async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> where Self: LoadRow; + + fn get_table_engine(_table: AnalyticsCollection) -> TableEngine { + TableEngine::BasicTree + } } pub trait LoadRow @@ -117,3 +133,5 @@ impl ErrorSwitch for QueryBuildingError { FiltersError::QueryBuildingError } } + +impl_misc_api_event_type!(AnalyticsDomain); diff --git a/crates/router/src/analytics/utils.rs b/crates/analytics/src/utils.rs similarity index 52% rename from crates/router/src/analytics/utils.rs rename to crates/analytics/src/utils.rs index f7e6ea69dc37..6a0aa973a1e7 100644 --- a/crates/router/src/analytics/utils.rs +++ b/crates/analytics/src/utils.rs @@ -1,6 +1,8 @@ use api_models::analytics::{ + api_event::{ApiEventDimensions, ApiEventMetrics}, payments::{PaymentDimensions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, + sdk_events::{SdkEventDimensions, SdkEventMetrics}, NameDescription, }; use strum::IntoEnumIterator; @@ -13,6 +15,14 @@ pub fn get_refund_dimensions() -> Vec { RefundDimensions::iter().map(Into::into).collect() } +pub fn get_sdk_event_dimensions() -> Vec { + SdkEventDimensions::iter().map(Into::into).collect() +} + +pub fn get_api_event_dimensions() -> Vec { + ApiEventDimensions::iter().map(Into::into).collect() +} + pub fn get_payment_metrics_info() -> Vec { PaymentMetrics::iter().map(Into::into).collect() } @@ -20,3 +30,11 @@ pub fn get_payment_metrics_info() -> Vec { pub fn get_refund_metrics_info() -> Vec { RefundMetrics::iter().map(Into::into).collect() } + +pub fn get_sdk_event_metrics_info() -> Vec { + SdkEventMetrics::iter().map(Into::into).collect() +} + +pub fn get_api_event_metrics_info() -> Vec { + ApiEventMetrics::iter().map(Into::into).collect() +} diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 0358b6b313cf..0263427b0fde 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -1,15 +1,20 @@ use std::collections::HashSet; -use common_utils::events::ApiEventMetric; -use time::PrimitiveDateTime; +use common_utils::pii::EmailStrategy; +use masking::Secret; use self::{ - payments::{PaymentDimensions, PaymentMetrics}, + api_event::{ApiEventDimensions, ApiEventMetrics}, + payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, + sdk_events::{SdkEventDimensions, SdkEventMetrics}, }; +pub use crate::payments::TimeRange; +pub mod api_event; pub mod payments; pub mod refunds; +pub mod sdk_events; #[derive(Debug, serde::Serialize)] pub struct NameDescription { @@ -25,23 +30,12 @@ pub struct GetInfoResponse { pub dimensions: Vec, } -impl ApiEventMetric for GetInfoResponse {} - -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase")] -pub struct TimeRange { - #[serde(with = "common_utils::custom_serde::iso8601")] - pub start_time: PrimitiveDateTime, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub end_time: Option, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub struct TimeSeries { pub granularity: Granularity, } -#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub enum Granularity { #[serde(rename = "G_ONEMIN")] OneMin, @@ -57,7 +51,7 @@ pub enum Granularity { OneDay, } -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentMetricRequest { pub time_series: Option, @@ -67,13 +61,51 @@ pub struct GetPaymentMetricRequest { #[serde(default)] pub filters: payments::PaymentFilters, pub metrics: HashSet, + pub distribution: Option, #[serde(default)] pub delta: bool, } -impl ApiEventMetric for GetPaymentMetricRequest {} +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub enum QueryLimit { + #[serde(rename = "TOP_5")] + Top5, + #[serde(rename = "TOP_10")] + Top10, +} + +#[allow(clippy::from_over_into)] +impl Into for QueryLimit { + fn into(self) -> u64 { + match self { + Self::Top5 => 5, + Self::Top10 => 10, + } + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Distribution { + pub distribution_for: PaymentDistributions, + pub distribution_cardinality: QueryLimit, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportRequest { + pub time_range: TimeRange, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateReportRequest { + pub request: ReportRequest, + pub merchant_id: String, + pub email: Secret, +} -#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetRefundMetricRequest { pub time_series: Option, @@ -87,14 +119,26 @@ pub struct GetRefundMetricRequest { pub delta: bool, } -impl ApiEventMetric for GetRefundMetricRequest {} +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSdkEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: sdk_events::SdkEventFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} #[derive(Debug, serde::Serialize)] pub struct AnalyticsMetadata { pub current_time_range: TimeRange, } -#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentFiltersRequest { pub time_range: TimeRange, @@ -102,16 +146,12 @@ pub struct GetPaymentFiltersRequest { pub group_by_names: Vec, } -impl ApiEventMetric for GetPaymentFiltersRequest {} - #[derive(Debug, Default, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct PaymentFiltersResponse { pub query_data: Vec, } -impl ApiEventMetric for PaymentFiltersResponse {} - #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct FilterValue { @@ -119,34 +159,88 @@ pub struct FilterValue { pub values: Vec, } -#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] + pub struct GetRefundFilterRequest { pub time_range: TimeRange, #[serde(default)] pub group_by_names: Vec, } -impl ApiEventMetric for GetRefundFilterRequest {} - #[derive(Debug, Default, serde::Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RefundFiltersResponse { pub query_data: Vec, } -impl ApiEventMetric for RefundFiltersResponse {} - #[derive(Debug, serde::Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] + pub struct RefundFilterValue { pub dimension: RefundDimensions, pub values: Vec, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSdkEventFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventFilterValue { + pub dimension: SdkEventDimensions, + pub values: Vec, +} + #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct MetricsResponse { pub query_data: Vec, pub meta_data: [AnalyticsMetadata; 1], } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetApiEventFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEventFiltersResponse { + pub query_data: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiEventFilterValue { + pub dimension: ApiEventDimensions, + pub values: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetApiEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: api_event::ApiEventFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} diff --git a/crates/api_models/src/analytics/api_event.rs b/crates/api_models/src/analytics/api_event.rs new file mode 100644 index 000000000000..62fe829f01b9 --- /dev/null +++ b/crates/api_models/src/analytics/api_event.rs @@ -0,0 +1,148 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ApiLogsRequest { + #[serde(flatten)] + pub query_param: QueryType, + pub api_name_filter: Option>, +} + +pub enum FilterType { + ApiCountFilter, + LatencyFilter, + StatusCodeFilter, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub enum QueryType { + Payment { + payment_id: String, + }, + Refund { + payment_id: String, + refund_id: String, + }, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ApiEventDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + StatusCode, + FlowType, + ApiFlow, +} + +impl From for NameDescription { + fn from(value: ApiEventDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct ApiEventFilters { + pub status_code: Vec, + pub flow_type: Vec, + pub api_flow: Vec, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ApiEventMetrics { + Latency, + ApiCount, + StatusCodeCount, +} + +impl From for NameDescription { + fn from(value: ApiEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct ApiEventMetricsBucketIdentifier { + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + // Coz FE sucks + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl ApiEventMetricsBucketIdentifier { + pub fn new(normalized_time_range: TimeRange) -> Self { + Self { + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +impl Hash for ApiEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.time_bucket.hash(state); + } +} + +impl PartialEq for ApiEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct ApiEventMetricsBucketValue { + pub latency: Option, + pub api_count: Option, + pub status_code_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct ApiMetricsBucketResponse { + #[serde(flatten)] + pub values: ApiEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: ApiEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index b5e5852d6283..2d7ae262f489 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -3,13 +3,12 @@ use std::{ hash::{Hash, Hasher}, }; -use common_enums::enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}; -use common_utils::events::ApiEventMetric; - use super::{NameDescription, TimeRange}; -use crate::{analytics::MetricsResponse, enums::Connector}; +use crate::enums::{ + AttemptStatus, AuthenticationType, Connector, Currency, PaymentMethod, PaymentMethodType, +}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct PaymentFilters { #[serde(default)] pub currency: Vec, @@ -21,6 +20,8 @@ pub struct PaymentFilters { pub auth_type: Vec, #[serde(default)] pub payment_method: Vec, + #[serde(default)] + pub payment_method_type: Vec, } #[derive( @@ -44,6 +45,7 @@ pub enum PaymentDimensions { // Consult the Dashboard FE folks since these also affects the order of metrics on FE Connector, PaymentMethod, + PaymentMethodType, Currency, #[strum(serialize = "authentication_type")] #[serde(rename = "authentication_type")] @@ -73,6 +75,35 @@ pub enum PaymentMetrics { PaymentSuccessCount, PaymentProcessedAmount, AvgTicketSize, + RetriesCount, + ConnectorSuccessRate, +} + +#[derive(Debug, Default, serde::Serialize)] +pub struct ErrorResult { + pub reason: String, + pub count: i64, + pub percentage: f64, +} + +#[derive( + Clone, + Copy, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PaymentDistributions { + #[strum(serialize = "error_message")] + PaymentErrorMessage, } pub mod metric_behaviour { @@ -109,6 +140,7 @@ pub struct PaymentMetricsBucketIdentifier { #[serde(rename = "authentication_type")] pub auth_type: Option, pub payment_method: Option, + pub payment_method_type: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, // Coz FE sucks @@ -124,6 +156,7 @@ impl PaymentMetricsBucketIdentifier { connector: Option, auth_type: Option, payment_method: Option, + payment_method_type: Option, normalized_time_range: TimeRange, ) -> Self { Self { @@ -132,6 +165,7 @@ impl PaymentMetricsBucketIdentifier { connector, auth_type, payment_method, + payment_method_type, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } @@ -145,6 +179,7 @@ impl Hash for PaymentMetricsBucketIdentifier { self.connector.hash(state); self.auth_type.map(|i| i.to_string()).hash(state); self.payment_method.hash(state); + self.payment_method_type.hash(state); self.time_bucket.hash(state); } } @@ -166,6 +201,10 @@ pub struct PaymentMetricsBucketValue { pub payment_success_count: Option, pub payment_processed_amount: Option, pub avg_ticket_size: Option, + pub payment_error_message: Option>, + pub retries_count: Option, + pub retries_amount_processed: Option, + pub connector_success_rate: Option, } #[derive(Debug, serde::Serialize)] @@ -175,6 +214,3 @@ pub struct MetricsBucketResponse { #[serde(flatten)] pub dimensions: PaymentMetricsBucketIdentifier, } - -impl ApiEventMetric for MetricsBucketResponse {} -impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index c5d444338d38..5ecdf1cecb3f 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -3,10 +3,7 @@ use std::{ hash::{Hash, Hasher}, }; -use common_enums::enums::{Currency, RefundStatus}; -use common_utils::events::ApiEventMetric; - -use crate::analytics::MetricsResponse; +use crate::{enums::Currency, refunds::RefundStatus}; #[derive( Clone, @@ -20,7 +17,7 @@ use crate::analytics::MetricsResponse; strum::Display, strum::EnumString, )] -// TODO RefundType common_enums need to mapped to storage_model +// TODO RefundType api_models_oss need to mapped to storage_model #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RefundType { @@ -31,7 +28,7 @@ pub enum RefundType { } use super::{NameDescription, TimeRange}; -#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct RefundFilters { #[serde(default)] pub currency: Vec, @@ -115,8 +112,9 @@ impl From for NameDescription { #[derive(Debug, serde::Serialize, Eq)] pub struct RefundMetricsBucketIdentifier { pub currency: Option, - pub refund_status: Option, + pub refund_status: Option, pub connector: Option, + pub refund_type: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, @@ -128,7 +126,7 @@ pub struct RefundMetricsBucketIdentifier { impl Hash for RefundMetricsBucketIdentifier { fn hash(&self, state: &mut H) { self.currency.hash(state); - self.refund_status.map(|i| i.to_string()).hash(state); + self.refund_status.hash(state); self.connector.hash(state); self.refund_type.hash(state); self.time_bucket.hash(state); @@ -147,7 +145,7 @@ impl PartialEq for RefundMetricsBucketIdentifier { impl RefundMetricsBucketIdentifier { pub fn new( currency: Option, - refund_status: Option, + refund_status: Option, connector: Option, refund_type: Option, normalized_time_range: TimeRange, @@ -162,7 +160,6 @@ impl RefundMetricsBucketIdentifier { } } } - #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketValue { pub refund_success_rate: Option, @@ -170,7 +167,6 @@ pub struct RefundMetricsBucketValue { pub refund_success_count: Option, pub refund_processed_amount: Option, } - #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketResponse { #[serde(flatten)] @@ -178,6 +174,3 @@ pub struct RefundMetricsBucketResponse { #[serde(flatten)] pub dimensions: RefundMetricsBucketIdentifier, } - -impl ApiEventMetric for RefundMetricsBucketResponse {} -impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/sdk_events.rs b/crates/api_models/src/analytics/sdk_events.rs new file mode 100644 index 000000000000..76ccb29867f2 --- /dev/null +++ b/crates/api_models/src/analytics/sdk_events.rs @@ -0,0 +1,215 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SdkEventsRequest { + pub payment_id: String, + pub time_range: TimeRange, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct SdkEventFilters { + #[serde(default)] + pub payment_method: Vec, + #[serde(default)] + pub platform: Vec, + #[serde(default)] + pub browser_name: Vec, + #[serde(default)] + pub source: Vec, + #[serde(default)] + pub component: Vec, + #[serde(default)] + pub payment_experience: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum SdkEventDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + PaymentMethod, + Platform, + BrowserName, + Source, + Component, + PaymentExperience, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum SdkEventMetrics { + PaymentAttempts, + PaymentSuccessCount, + PaymentMethodsCallCount, + SdkRenderedCount, + SdkInitiatedCount, + PaymentMethodSelectedCount, + PaymentDataFilledCount, + AveragePaymentTime, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SdkEventNames { + StripeElementsCalled, + AppRendered, + PaymentMethodChanged, + PaymentDataFilled, + PaymentAttempt, + PaymentSuccess, + PaymentMethodsCall, + ConfirmCall, + SessionsCall, + CustomerPaymentMethodsCall, + RedirectingUser, + DisplayBankTransferInfoPage, + DisplayQrCodeInfoPage, +} + +pub mod metric_behaviour { + pub struct PaymentAttempts; + pub struct PaymentSuccessCount; + pub struct PaymentMethodsCallCount; + pub struct SdkRenderedCount; + pub struct SdkInitiatedCount; + pub struct PaymentMethodSelectedCount; + pub struct PaymentDataFilledCount; + pub struct AveragePaymentTime; +} + +impl From for NameDescription { + fn from(value: SdkEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: SdkEventDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct SdkEventMetricsBucketIdentifier { + pub payment_method: Option, + pub platform: Option, + pub browser_name: Option, + pub source: Option, + pub component: Option, + pub payment_experience: Option, + pub time_bucket: Option, +} + +impl SdkEventMetricsBucketIdentifier { + pub fn new( + payment_method: Option, + platform: Option, + browser_name: Option, + source: Option, + component: Option, + payment_experience: Option, + time_bucket: Option, + ) -> Self { + Self { + payment_method, + platform, + browser_name, + source, + component, + payment_experience, + time_bucket, + } + } +} + +impl Hash for SdkEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.payment_method.hash(state); + self.platform.hash(state); + self.browser_name.hash(state); + self.source.hash(state); + self.component.hash(state); + self.payment_experience.hash(state); + self.time_bucket.hash(state); + } +} + +impl PartialEq for SdkEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct SdkEventMetricsBucketValue { + pub payment_attempts: Option, + pub payment_success_count: Option, + pub payment_methods_call_count: Option, + pub average_payment_time: Option, + pub sdk_rendered_count: Option, + pub sdk_initiated_count: Option, + pub payment_method_selected_count: Option, + pub payment_data_filled_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: SdkEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: SdkEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 782c02be7a3a..345f827daeac 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -14,8 +14,16 @@ use common_utils::{ }; use crate::{ - admin::*, api_keys::*, cards_info::*, disputes::*, files::*, mandates::*, payment_methods::*, - payments::*, verifications::*, + admin::*, + analytics::{api_event::*, sdk_events::*, *}, + api_keys::*, + cards_info::*, + disputes::*, + files::*, + mandates::*, + payment_methods::*, + payments::*, + verifications::*, }; impl ApiEventMetric for TimeRange {} @@ -63,7 +71,23 @@ impl_misc_api_event_type!( ApplepayMerchantVerificationRequest, ApplepayMerchantResponse, ApplepayVerifiedDomainsResponse, - UpdateApiKeyRequest + UpdateApiKeyRequest, + GetApiEventFiltersRequest, + ApiEventFiltersResponse, + GetInfoResponse, + GetPaymentMetricRequest, + GetRefundMetricRequest, + GetSdkEventMetricRequest, + GetPaymentFiltersRequest, + PaymentFiltersResponse, + GetRefundFilterRequest, + RefundFiltersResponse, + GetSdkEventFiltersRequest, + SdkEventFiltersResponse, + ApiLogsRequest, + GetApiEventMetricRequest, + SdkEventsRequest, + ReportRequest ); #[cfg(feature = "stripe")] @@ -76,3 +100,9 @@ impl_misc_api_event_type!( CustomerPaymentMethodListResponse, CreateCustomerResponse ); + +impl ApiEventMetric for MetricsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index acb9bbdd6cd4..bd4c59211e24 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2339,9 +2339,11 @@ pub struct PaymentListFilters { pub struct TimeRange { /// The start time to filter payments list or to get list of filters. To get list of filters start time is needed to be passed #[serde(with = "common_utils::custom_serde::iso8601")] + #[serde(alias = "startTime")] pub start_time: PrimitiveDateTime, /// The end time to filter payments list or to get list of filters. If not passed the default time is now #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(alias = "endTime")] pub end_time: Option, } diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 57ae1ec1ec87..857d53b6999e 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -18,7 +18,6 @@ common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } - # Third party deps async-trait = "0.1.68" error-stack = "0.3.1" diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b51dc045b20d..f508460574dd 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -16,7 +16,7 @@ email = ["external_services/email", "dep:aws-config", "olap"] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] -olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] +olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] @@ -102,6 +102,7 @@ tracing-futures = { version = "0.2.5", features = ["tokio"] } # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } +analytics = { version = "0.1.0", path = "../analytics", optional = true } cards = { version = "0.1.0", path = "../cards" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] } @@ -118,6 +119,7 @@ router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } erased-serde = "0.3.31" +rdkafka = "0.36.0" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d57403d92989..f31e908e0dc3 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1,129 +1,560 @@ -mod core; -mod errors; -pub mod metrics; -mod payments; -mod query; -mod refunds; -pub mod routes; - -mod sqlx; -mod types; -mod utils; - -use api_models::analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, - refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, - Granularity, TimeRange, -}; -use router_env::{instrument, tracing}; - -use self::{ - payments::metrics::{PaymentMetric, PaymentMetricRow}, - refunds::metrics::{RefundMetric, RefundMetricRow}, - sqlx::SqlxClient, -}; -use crate::configs::settings::Database; - -#[derive(Clone, Debug)] -pub enum AnalyticsProvider { - Sqlx(SqlxClient), -} +pub use analytics::*; + +pub mod routes { + use actix_web::{web, Responder, Scope}; + use analytics::{ + api_event::api_events_core, errors::AnalyticsError, lambda_utils::invoke_lambda, + sdk_events::sdk_events_core, + }; + use api_models::analytics::{ + GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, + GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, + GetRefundMetricRequest, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, + }; + use error_stack::ResultExt; + use router_env::AnalyticsFlow; + + use crate::{ + core::api_locking, + db::user::UserInterface, + routes::AppState, + services::{ + api, + authentication::{self as auth, AuthToken, AuthenticationData}, + authorization::permissions::Permission, + ApplicationResponse, + }, + types::domain::UserEmail, + }; + + pub struct Analytics; + + impl Analytics { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/analytics/v1").app_data(web::Data::new(state)); + { + route = route + .service( + web::resource("metrics/payments") + .route(web::post().to(get_payment_metrics)), + ) + .service( + web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics)), + ) + .service( + web::resource("filters/payments") + .route(web::post().to(get_payment_filters)), + ) + .service( + web::resource("filters/refunds").route(web::post().to(get_refund_filters)), + ) + .service(web::resource("{domain}/info").route(web::get().to(get_info))) + .service( + web::resource("report/dispute") + .route(web::post().to(generate_dispute_report)), + ) + .service( + web::resource("report/refunds") + .route(web::post().to(generate_refund_report)), + ) + .service( + web::resource("report/payments") + .route(web::post().to(generate_payment_report)), + ) + .service( + web::resource("metrics/sdk_events") + .route(web::post().to(get_sdk_event_metrics)), + ) + .service( + web::resource("filters/sdk_events") + .route(web::post().to(get_sdk_event_filters)), + ) + .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) + .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) + .service( + web::resource("filters/api_events") + .route(web::post().to(get_api_event_filters)), + ) + .service( + web::resource("metrics/api_events") + .route(web::post().to(get_api_events_metrics)), + ) + } + route + } + } -impl Default for AnalyticsProvider { - fn default() -> Self { - Self::Sqlx(SqlxClient::default()) + pub async fn get_info( + state: web::Data, + req: actix_web::HttpRequest, + domain: actix_web::web::Path, + ) -> impl Responder { + let flow = AnalyticsFlow::GetInfo; + Box::pin(api::server_wrap( + flow, + state, + &req, + domain.into_inner(), + |_, _, domain| async { + analytics::core::get_domain_info(domain) + .await + .map(ApplicationResponse::Json) + }, + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await } -} -impl AnalyticsProvider { - #[instrument(skip_all)] + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. pub async fn get_payment_metrics( - &self, - metric: &PaymentMetrics, - dimensions: &[PaymentDimensions], - merchant_id: &str, - filters: &PaymentFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - // Metrics to get the fetch time for each payment metric - metrics::request::record_operation_time( - async { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) - .await - } - } + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetPaymentMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetPaymentMetricRequest"); + let flow = AnalyticsFlow::GetPaymentMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::payments::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) }, - &metrics::METRIC_FETCH_TIME, - metric, - self, - ) + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) .await } - pub async fn get_refund_metrics( - &self, - metric: &RefundMetrics, - dimensions: &[RefundDimensions], - merchant_id: &str, - filters: &RefundFilters, - granularity: &Option, - time_range: &TimeRange, - ) -> types::MetricsResult> { - match self { - Self::Sqlx(pool) => { - metric - .load_metrics( - dimensions, - merchant_id, - filters, - granularity, - time_range, - pool, - ) + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. + pub async fn get_refunds_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetRefundMetricRequest; 1]>, + ) -> impl Responder { + #[allow(clippy::expect_used)] + // safety: This shouldn't panic owing to the data type + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetRefundMetricRequest"); + let flow = AnalyticsFlow::GetRefundsMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::refunds::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetSdkEventMetricRequest` element. + pub async fn get_sdk_event_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetSdkEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetSdkEventMetricRequest"); + let flow = AnalyticsFlow::GetSdkMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::sdk_events::get_metrics( + &state.pool, + auth.merchant_account.publishable_key.as_ref(), + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_payment_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetPaymentFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::payments::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_refund_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetRefundFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req: GetRefundFilterRequest| async move { + analytics::refunds::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_sdk_event_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSdkEventFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::sdk_events::get_filters( + &state.pool, + req, + auth.merchant_account.publishable_key.as_ref(), + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_api_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query, + ) -> impl Responder { + let flow = AnalyticsFlow::GetApiEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + api_events_core(&state.pool, req, auth.merchant_account.merchant_id) .await - } - } + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } - pub async fn from_conf( - config: &AnalyticsConfig, - #[cfg(feature = "kms")] kms_client: &external_services::kms::KmsClient, - ) -> Self { - match config { - AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx( - SqlxClient::from_conf( - sqlx, - #[cfg(feature = "kms")] - kms_client, + pub async fn get_sdk_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSdkEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + sdk_events_core( + &state.pool, + req, + auth.merchant_account.publishable_key.unwrap_or_default(), ) - .await, - ), - } + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } -} -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(tag = "source")] -#[serde(rename_all = "lowercase")] -pub enum AnalyticsConfig { - Sqlx { sqlx: Database }, -} + pub async fn generate_refund_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); -impl Default for AnalyticsConfig { - fn default() -> Self { - Self::Sqlx { - sqlx: Database::default(), - } + let flow = AnalyticsFlow::GenerateRefundReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.refund_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_dispute_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); + + let flow = AnalyticsFlow::GenerateDisputeReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.dispute_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn generate_payment_report( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let state_ref = &state; + let req_headers = &req.headers(); + + let flow = AnalyticsFlow::GeneratePaymentReport; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, payload| async move { + let jwt_payload = + auth::parse_jwt_payload::(req_headers, state_ref).await; + + let user_id = jwt_payload + .change_context(AnalyticsError::UnknownError)? + .user_id; + + let user = UserInterface::find_user_by_id(&*state.store, &user_id) + .await + .change_context(AnalyticsError::UnknownError)?; + + let user_email = UserEmail::from_pii_email(user.email) + .change_context(AnalyticsError::UnknownError)? + .get_secret(); + + let lambda_req = GenerateReportRequest { + request: payload, + merchant_id: auth.merchant_account.merchant_id.to_string(), + email: user_email, + }; + + let json_bytes = + serde_json::to_vec(&lambda_req).map_err(|_| AnalyticsError::UnknownError)?; + invoke_lambda( + &state.conf.report_download_config.payment_function, + &state.conf.report_download_config.region, + &json_bytes, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetApiEventMetricRequest` element. + pub async fn get_api_events_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetApiEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetApiEventMetricRequest"); + let flow = AnalyticsFlow::GetApiEventMetrics; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + |state, auth: AuthenticationData, req| async move { + analytics::api_event::get_api_event_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + + pub async fn get_api_event_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetApiEventFilters; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + analytics::api_event::get_filters( + &state.pool, + req, + auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await } } diff --git a/crates/router/src/analytics/core.rs b/crates/router/src/analytics/core.rs deleted file mode 100644 index bf124a6c0e85..000000000000 --- a/crates/router/src/analytics/core.rs +++ /dev/null @@ -1,96 +0,0 @@ -use api_models::analytics::{ - payments::PaymentDimensions, refunds::RefundDimensions, FilterValue, GetInfoResponse, - GetPaymentFiltersRequest, GetRefundFilterRequest, PaymentFiltersResponse, RefundFilterValue, - RefundFiltersResponse, -}; -use error_stack::ResultExt; - -use super::{ - errors::{self, AnalyticsError}, - payments::filters::{get_payment_filter_for_dimension, FilterRow}, - refunds::filters::{get_refund_filter_for_dimension, RefundFilterRow}, - types::AnalyticsDomain, - utils, AnalyticsProvider, -}; -use crate::{services::ApplicationResponse, types::domain}; - -pub type AnalyticsApiResponse = errors::AnalyticsResult>; - -pub async fn get_domain_info(domain: AnalyticsDomain) -> AnalyticsApiResponse { - let info = match domain { - AnalyticsDomain::Payments => GetInfoResponse { - metrics: utils::get_payment_metrics_info(), - download_dimensions: None, - dimensions: utils::get_payment_dimensions(), - }, - AnalyticsDomain::Refunds => GetInfoResponse { - metrics: utils::get_refund_metrics_info(), - download_dimensions: None, - dimensions: utils::get_refund_dimensions(), - }, - }; - Ok(ApplicationResponse::Json(info)) -} - -pub async fn payment_filters_core( - pool: AnalyticsProvider, - req: GetPaymentFiltersRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = PaymentFiltersResponse::default(); - - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_payment_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: FilterRow| match dim { - PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), - PaymentDimensions::Connector => fil.connector, - PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), - PaymentDimensions::PaymentMethod => fil.payment_method, - }) - .collect::>(); - res.query_data.push(FilterValue { - dimension: dim, - values, - }) - } - - Ok(ApplicationResponse::Json(res)) -} - -pub async fn refund_filter_core( - pool: AnalyticsProvider, - req: GetRefundFilterRequest, - merchant: domain::MerchantAccount, -) -> AnalyticsApiResponse { - let mut res = RefundFiltersResponse::default(); - for dim in req.group_by_names { - let values = match pool.clone() { - AnalyticsProvider::Sqlx(pool) => { - get_refund_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) - .await - } - } - .change_context(AnalyticsError::UnknownError)? - .into_iter() - .filter_map(|fil: RefundFilterRow| match dim { - RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), - RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), - RefundDimensions::Connector => fil.connector, - RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), - }) - .collect::>(); - res.query_data.push(RefundFilterValue { - dimension: dim, - values, - }) - } - Ok(ApplicationResponse::Json(res)) -} diff --git a/crates/router/src/analytics/payments.rs b/crates/router/src/analytics/payments.rs deleted file mode 100644 index 527bf75a3c72..000000000000 --- a/crates/router/src/analytics/payments.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod accumulator; -mod core; -pub mod filters; -pub mod metrics; -pub mod types; -pub use accumulator::{PaymentMetricAccumulator, PaymentMetricsAccumulator}; - -pub trait PaymentAnalytics: - metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics -{ -} - -pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/payments/core.rs b/crates/router/src/analytics/payments/core.rs deleted file mode 100644 index 23eca8879a70..000000000000 --- a/crates/router/src/analytics/payments/core.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - payments::{MetricsBucketResponse, PaymentMetrics, PaymentMetricsBucketIdentifier}, - AnalyticsMetadata, GetPaymentMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - instrument, logger, - tracing::{self, Instrument}, -}; - -use super::PaymentMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, metrics, - payments::PaymentMetricAccumulator, AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -#[instrument(skip_all)] -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetPaymentMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap< - PaymentMetricsBucketIdentifier, - PaymentMetricsAccumulator, - > = HashMap::new(); - - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_payments_query", - payment_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_payment_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - let data = data?; - let attributes = &[ - metrics::request::add_attributes("metric_type", metric.to_string()), - metrics::request::add_attributes( - "source", - match pool { - crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", - }, - ), - ]; - - let value = u64::try_from(data.len()); - if let Ok(val) = value { - metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); - logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); - } - - for (id, value) in data { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - PaymentMetrics::PaymentSuccessRate => metrics_builder - .payment_success_rate - .add_metrics_bucket(&value), - PaymentMetrics::PaymentCount => { - metrics_builder.payment_count.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentSuccessCount => { - metrics_builder.payment_success.add_metrics_bucket(&value) - } - PaymentMetrics::PaymentProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - PaymentMetrics::AvgTicketSize => { - metrics_builder.avg_ticket_size.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| MetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/refunds/core.rs b/crates/router/src/analytics/refunds/core.rs deleted file mode 100644 index 4c2d2c394181..000000000000 --- a/crates/router/src/analytics/refunds/core.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::collections::HashMap; - -use api_models::analytics::{ - refunds::{RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse}, - AnalyticsMetadata, GetRefundMetricRequest, MetricsResponse, -}; -use error_stack::{IntoReport, ResultExt}; -use router_env::{ - logger, - tracing::{self, Instrument}, -}; - -use super::RefundMetricsAccumulator; -use crate::{ - analytics::{ - core::AnalyticsApiResponse, errors::AnalyticsError, refunds::RefundMetricAccumulator, - AnalyticsProvider, - }, - services::ApplicationResponse, - types::domain, -}; - -pub async fn get_metrics( - pool: AnalyticsProvider, - merchant_account: domain::MerchantAccount, - req: GetRefundMetricRequest, -) -> AnalyticsApiResponse> { - let mut metrics_accumulator: HashMap = - HashMap::new(); - let mut set = tokio::task::JoinSet::new(); - for metric_type in req.metrics.iter().cloned() { - let req = req.clone(); - let merchant_id = merchant_account.merchant_id.clone(); - let pool = pool.clone(); - let task_span = tracing::debug_span!( - "analytics_refund_query", - refund_metric = metric_type.as_ref() - ); - set.spawn( - async move { - let data = pool - .get_refund_metrics( - &metric_type, - &req.group_by_names.clone(), - &merchant_id, - &req.filters, - &req.time_series.map(|t| t.granularity), - &req.time_range, - ) - .await - .change_context(AnalyticsError::UnknownError); - (metric_type, data) - } - .instrument(task_span), - ); - } - - while let Some((metric, data)) = set - .join_next() - .await - .transpose() - .into_report() - .change_context(AnalyticsError::UnknownError)? - { - for (id, value) in data? { - logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); - let metrics_builder = metrics_accumulator.entry(id).or_default(); - match metric { - RefundMetrics::RefundSuccessRate => metrics_builder - .refund_success_rate - .add_metrics_bucket(&value), - RefundMetrics::RefundCount => { - metrics_builder.refund_count.add_metrics_bucket(&value) - } - RefundMetrics::RefundSuccessCount => { - metrics_builder.refund_success.add_metrics_bucket(&value) - } - RefundMetrics::RefundProcessedAmount => { - metrics_builder.processed_amount.add_metrics_bucket(&value) - } - } - } - - logger::debug!( - "Analytics Accumulated Results: metric: {}, results: {:#?}", - metric, - metrics_accumulator - ); - } - let query_data: Vec = metrics_accumulator - .into_iter() - .map(|(id, val)| RefundMetricsBucketResponse { - values: val.collect(), - dimensions: id, - }) - .collect(); - - Ok(ApplicationResponse::Json(MetricsResponse { - query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, - }], - })) -} diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs deleted file mode 100644 index 113312cdf10f..000000000000 --- a/crates/router/src/analytics/routes.rs +++ /dev/null @@ -1,164 +0,0 @@ -use actix_web::{web, Responder, Scope}; -use api_models::analytics::{ - GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, - GetRefundMetricRequest, -}; -use router_env::AnalyticsFlow; - -use super::{core::*, payments, refunds, types::AnalyticsDomain}; -use crate::{ - core::api_locking, - services::{ - api, authentication as auth, authentication::AuthenticationData, - authorization::permissions::Permission, - }, - AppState, -}; - -pub struct Analytics; - -impl Analytics { - pub fn server(state: AppState) -> Scope { - let route = web::scope("/analytics/v1").app_data(web::Data::new(state)); - route - .service(web::resource("metrics/payments").route(web::post().to(get_payment_metrics))) - .service(web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics))) - .service(web::resource("filters/payments").route(web::post().to(get_payment_filters))) - .service(web::resource("filters/refunds").route(web::post().to(get_refund_filters))) - .service(web::resource("{domain}/info").route(web::get().to(get_info))) - } -} - -pub async fn get_info( - state: web::Data, - req: actix_web::HttpRequest, - domain: actix_web::web::Path, -) -> impl Responder { - let flow = AnalyticsFlow::GetInfo; - api::server_wrap( - flow, - state, - &req, - domain.into_inner(), - |_, _, domain| get_domain_info(domain), - &auth::NoAuth, - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. -pub async fn get_payment_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetPaymentMetricRequest; 1]>, -) -> impl Responder { - // safety: This shouldn't panic owing to the data type - #[allow(clippy::expect_used)] - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetPaymentMetricRequest"); - let flow = AnalyticsFlow::GetPaymentMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - payments::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} - -/// # Panics -/// -/// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. -pub async fn get_refunds_metrics( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json<[GetRefundMetricRequest; 1]>, -) -> impl Responder { - #[allow(clippy::expect_used)] - // safety: This shouldn't panic owing to the data type - let payload = json_payload - .into_inner() - .to_vec() - .pop() - .expect("Couldn't get GetRefundMetricRequest"); - let flow = AnalyticsFlow::GetRefundsMetrics; - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: AuthenticationData, req| { - refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_payment_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetPaymentFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req| { - payment_filters_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} - -pub async fn get_refund_filters( - state: web::Data, - req: actix_web::HttpRequest, - json_payload: web::Json, -) -> impl Responder { - let flow = AnalyticsFlow::GetRefundFilters; - api::server_wrap( - flow, - state, - &req, - json_payload.into_inner(), - |state, auth: AuthenticationData, req: GetRefundFilterRequest| { - refund_filter_core(state.pool.clone(), req, auth.merchant_account) - }, - auth::auth_type( - &auth::ApiKeyAuth, - &auth::JWTAuth(Permission::Analytics), - req.headers(), - ), - api_locking::LockAction::NotApplicable, - ) - .await -} diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 4c19408582bc..32e9cfc6ca29 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -20,7 +20,6 @@ use strum::EnumString; use tokio::sync::{mpsc, oneshot}; const SCHEDULER_FLOW: &str = "SCHEDULER_FLOW"; - #[tokio::main] async fn main() -> CustomResult<(), ProcessTrackerError> { // console_subscriber::init(); @@ -30,7 +29,6 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { #[allow(clippy::expect_used)] let conf = Settings::with_config_path(cmd_line.config_path) .expect("Unable to construct application configuration"); - let api_client = Box::new( services::ProxyClient::new( conf.proxy.clone(), diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index c2f159d16cf1..37f2d15774a5 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -63,7 +63,7 @@ impl KmsDecrypt for settings::Database { password: self.password.decrypt_inner(kms_client).await?.into(), pool_size: self.pool_size, connection_timeout: self.connection_timeout, - queue_strategy: self.queue_strategy.into(), + queue_strategy: self.queue_strategy, min_idle: self.min_idle, max_lifetime: self.max_lifetime, }) diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 918ae6647eef..f2d962b0abee 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -4,6 +4,8 @@ use std::{ str::FromStr, }; +#[cfg(feature = "olap")] +use analytics::ReportConfig; use api_models::{enums, payment_methods::RequiredFieldInfo}; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; @@ -16,12 +18,14 @@ pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use rust_decimal::Decimal; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; +use storage_impl::config::QueueStrategy; #[cfg(feature = "olap")] use crate::analytics::AnalyticsConfig; use crate::{ core::errors::{ApplicationError, ApplicationResult}, env::{self, logger, Env}, + events::EventsConfig, }; #[cfg(feature = "kms")] pub type Password = kms::KmsValue; @@ -109,6 +113,9 @@ pub struct Settings { pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, + #[cfg(feature = "olap")] + pub report_download_config: ReportConfig, + pub events: EventsConfig, } #[derive(Debug, Deserialize, Clone)] @@ -521,23 +528,6 @@ pub struct Database { pub max_lifetime: Option, } -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(rename_all = "PascalCase")] -pub enum QueueStrategy { - #[default] - Fifo, - Lifo, -} - -impl From for bb8::QueueStrategy { - fn from(value: QueueStrategy) -> Self { - match value { - QueueStrategy::Fifo => Self::Fifo, - QueueStrategy::Lifo => Self::Lifo, - } - } -} - #[cfg(not(feature = "kms"))] impl From for storage_impl::config::Database { fn from(val: Database) -> Self { @@ -837,6 +827,7 @@ impl Settings { #[cfg(feature = "s3")] self.file_upload_config.validate()?; self.lock_settings.validate()?; + self.events.validate()?; Ok(()) } } diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 2d572cee9513..33435bb0ad96 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -927,7 +927,9 @@ pub async fn start_refund_workflow( ) -> Result<(), errors::ProcessTrackerError> { match refund_tracker.name.as_deref() { Some("EXECUTE_REFUND") => trigger_refund_execute_workflow(state, refund_tracker).await, - Some("SYNC_REFUND") => sync_refund_with_gateway_workflow(state, refund_tracker).await, + Some("SYNC_REFUND") => { + Box::pin(sync_refund_with_gateway_workflow(state, refund_tracker)).await + } _ => Err(errors::ProcessTrackerError::JobNotFound), } } diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 67154ae33aef..be8d118a47c2 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -905,6 +905,7 @@ pub async fn webhooks_wrapper { diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 9687f7f97c92..549bda78eda8 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -12,6 +12,7 @@ pub mod events; pub mod file; pub mod fraud_check; pub mod gsm; +mod kafka_store; pub mod locker_mock_up; pub mod mandate; pub mod merchant_account; @@ -31,11 +32,24 @@ pub mod user_role; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; +use diesel_models::{ + fraud_check::{FraudCheck, FraudCheckNew, FraudCheckUpdate}, + organization::{Organization, OrganizationNew, OrganizationUpdate}, +}; +use error_stack::ResultExt; use masking::PeekInterface; use redis_interface::errors::RedisError; -use storage_impl::{redis::kv_store::RedisConnInterface, MockDb}; - -use crate::{errors::CustomResult, services::Store}; +use storage_impl::{errors::StorageError, redis::kv_store::RedisConnInterface, MockDb}; + +pub use self::kafka_store::KafkaStore; +use self::{fraud_check::FraudCheckInterface, organization::OrganizationInterface}; +pub use crate::{ + errors::CustomResult, + services::{ + kafka::{KafkaError, KafkaProducer, MQResult}, + Store, + }, +}; #[derive(PartialEq, Eq)] pub enum StorageImpl { @@ -58,7 +72,7 @@ pub trait StorageInterface: + ephemeral_key::EphemeralKeyInterface + events::EventInterface + file::FileMetadataInterface - + fraud_check::FraudCheckInterface + + FraudCheckInterface + locker_mock_up::LockerMockUpInterface + mandate::MandateInterface + merchant_account::MerchantAccountInterface @@ -79,7 +93,7 @@ pub trait StorageInterface: + RedisConnInterface + RequestIdStore + business_profile::BusinessProfileInterface - + organization::OrganizationInterface + + OrganizationInterface + routing_algorithm::RoutingAlgorithmInterface + gsm::GsmInterface + user::UserInterface @@ -151,7 +165,6 @@ where T: serde::de::DeserializeOwned, { use common_utils::ext_traits::ByteSliceExt; - use error_stack::ResultExt; let bytes = db.get_key(key).await?; bytes @@ -160,3 +173,72 @@ where } dyn_clone::clone_trait_object!(StorageInterface); + +impl RequestIdStore for KafkaStore { + fn add_request_id(&mut self, request_id: String) { + self.diesel_store.add_request_id(request_id) + } +} + +#[async_trait::async_trait] +impl FraudCheckInterface for KafkaStore { + async fn insert_fraud_check_response( + &self, + new: FraudCheckNew, + ) -> CustomResult { + self.diesel_store.insert_fraud_check_response(new).await + } + async fn update_fraud_check_response_with_attempt_id( + &self, + fraud_check: FraudCheck, + fraud_check_update: FraudCheckUpdate, + ) -> CustomResult { + self.diesel_store + .update_fraud_check_response_with_attempt_id(fraud_check, fraud_check_update) + .await + } + async fn find_fraud_check_by_payment_id( + &self, + payment_id: String, + merchant_id: String, + ) -> CustomResult { + self.diesel_store + .find_fraud_check_by_payment_id(payment_id, merchant_id) + .await + } + async fn find_fraud_check_by_payment_id_if_present( + &self, + payment_id: String, + merchant_id: String, + ) -> CustomResult, StorageError> { + self.diesel_store + .find_fraud_check_by_payment_id_if_present(payment_id, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl OrganizationInterface for KafkaStore { + async fn insert_organization( + &self, + organization: OrganizationNew, + ) -> CustomResult { + self.diesel_store.insert_organization(organization).await + } + async fn find_organization_by_org_id( + &self, + org_id: &str, + ) -> CustomResult { + self.diesel_store.find_organization_by_org_id(org_id).await + } + + async fn update_organization_by_org_id( + &self, + org_id: &str, + update: OrganizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_organization_by_org_id(org_id, update) + .await + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs new file mode 100644 index 000000000000..9cf1a7b80b8b --- /dev/null +++ b/crates/router/src/db/kafka_store.rs @@ -0,0 +1,1917 @@ +use std::sync::Arc; + +use common_enums::enums::MerchantStorageScheme; +use common_utils::errors::CustomResult; +use data_models::payments::{ + payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, +}; +use diesel_models::{ + enums::ProcessTrackerStatus, + ephemeral_key::{EphemeralKey, EphemeralKeyNew}, + reverse_lookup::{ReverseLookup, ReverseLookupNew}, + user_role as user_storage, +}; +use masking::Secret; +use redis_interface::{errors::RedisError, RedisConnectionPool, RedisEntryId}; +use router_env::logger; +use scheduler::{ + db::{process_tracker::ProcessTrackerInterface, queue::QueueInterface}, + SchedulerInterface, +}; +use storage_impl::redis::kv_store::RedisConnInterface; +use time::PrimitiveDateTime; + +use super::{user::UserInterface, user_role::UserRoleInterface}; +use crate::{ + core::errors::{self, ProcessTrackerError}, + db::{ + address::AddressInterface, + api_keys::ApiKeyInterface, + business_profile::BusinessProfileInterface, + capture::CaptureInterface, + cards_info::CardsInfoInterface, + configs::ConfigInterface, + customers::CustomerInterface, + dispute::DisputeInterface, + ephemeral_key::EphemeralKeyInterface, + events::EventInterface, + file::FileMetadataInterface, + gsm::GsmInterface, + locker_mock_up::LockerMockUpInterface, + mandate::MandateInterface, + merchant_account::MerchantAccountInterface, + merchant_connector_account::{ConnectorAccessToken, MerchantConnectorAccountInterface}, + merchant_key_store::MerchantKeyStoreInterface, + payment_link::PaymentLinkInterface, + payment_method::PaymentMethodInterface, + payout_attempt::PayoutAttemptInterface, + payouts::PayoutsInterface, + refund::RefundInterface, + reverse_lookup::ReverseLookupInterface, + routing_algorithm::RoutingAlgorithmInterface, + MasterKeyInterface, StorageInterface, + }, + services::{authentication, kafka::KafkaProducer, Store}, + types::{ + domain, + storage::{self, business_profile}, + AccessToken, + }, +}; + +#[derive(Clone)] +pub struct KafkaStore { + kafka_producer: KafkaProducer, + pub diesel_store: Store, +} + +impl KafkaStore { + pub async fn new(store: Store, kafka_producer: KafkaProducer) -> Self { + Self { + kafka_producer, + diesel_store: store, + } + } +} + +#[async_trait::async_trait] +impl AddressInterface for KafkaStore { + async fn find_address_by_address_id( + &self, + address_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_address_by_address_id(address_id, key_store) + .await + } + + async fn update_address( + &self, + address_id: String, + address: storage::AddressUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_address(address_id, address, key_store) + .await + } + + async fn update_address_for_payments( + &self, + this: domain::Address, + address: domain::AddressUpdate, + payment_id: String, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_address_for_payments(this, address, payment_id, key_store, storage_scheme) + .await + } + + async fn insert_address_for_payments( + &self, + payment_id: &str, + address: domain::Address, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_address_for_payments(payment_id, address, key_store, storage_scheme) + .await + } + + async fn find_address_by_merchant_id_payment_id_address_id( + &self, + merchant_id: &str, + payment_id: &str, + address_id: &str, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_address_by_merchant_id_payment_id_address_id( + merchant_id, + payment_id, + address_id, + key_store, + storage_scheme, + ) + .await + } + + async fn insert_address_for_customers( + &self, + address: domain::Address, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_address_for_customers(address, key_store) + .await + } + + async fn update_address_by_merchant_id_customer_id( + &self, + customer_id: &str, + merchant_id: &str, + address: storage::AddressUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .update_address_by_merchant_id_customer_id(customer_id, merchant_id, address, key_store) + .await + } +} + +#[async_trait::async_trait] +impl ApiKeyInterface for KafkaStore { + async fn insert_api_key( + &self, + api_key: storage::ApiKeyNew, + ) -> CustomResult { + self.diesel_store.insert_api_key(api_key).await + } + + async fn update_api_key( + &self, + merchant_id: String, + key_id: String, + api_key: storage::ApiKeyUpdate, + ) -> CustomResult { + self.diesel_store + .update_api_key(merchant_id, key_id, api_key) + .await + } + + async fn revoke_api_key( + &self, + merchant_id: &str, + key_id: &str, + ) -> CustomResult { + self.diesel_store.revoke_api_key(merchant_id, key_id).await + } + + async fn find_api_key_by_merchant_id_key_id_optional( + &self, + merchant_id: &str, + key_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_api_key_by_merchant_id_key_id_optional(merchant_id, key_id) + .await + } + + async fn find_api_key_by_hash_optional( + &self, + hashed_api_key: storage::HashedApiKey, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_api_key_by_hash_optional(hashed_api_key) + .await + } + + async fn list_api_keys_by_merchant_id( + &self, + merchant_id: &str, + limit: Option, + offset: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_api_keys_by_merchant_id(merchant_id, limit, offset) + .await + } +} + +#[async_trait::async_trait] +impl CardsInfoInterface for KafkaStore { + async fn get_card_info( + &self, + card_iin: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.get_card_info(card_iin).await + } +} + +#[async_trait::async_trait] +impl ConfigInterface for KafkaStore { + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult { + self.diesel_store.insert_config(config).await + } + + async fn find_config_by_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.find_config_by_key(key).await + } + + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.find_config_by_key_from_db(key).await + } + + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.diesel_store + .update_config_in_database(key, config_update) + .await + } + + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.diesel_store + .update_config_by_key(key, config_update) + .await + } + + async fn delete_config_by_key(&self, key: &str) -> CustomResult { + self.diesel_store.delete_config_by_key(key).await + } + + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + default_config: Option, + ) -> CustomResult { + self.diesel_store + .find_config_by_key_unwrap_or(key, default_config) + .await + } +} + +#[async_trait::async_trait] +impl CustomerInterface for KafkaStore { + async fn delete_customer_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_customer_by_customer_id_merchant_id(customer_id, merchant_id) + .await + } + + async fn find_customer_optional_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_customer_optional_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + } + + async fn update_customer_by_customer_id_merchant_id( + &self, + customer_id: String, + merchant_id: String, + customer: storage::CustomerUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_customer_by_customer_id_merchant_id( + customer_id, + merchant_id, + customer, + key_store, + ) + .await + } + + async fn list_customers_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_customers_by_merchant_id(merchant_id, key_store) + .await + } + + async fn find_customer_by_customer_id_merchant_id( + &self, + customer_id: &str, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_customer_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + } + + async fn insert_customer( + &self, + customer_data: domain::Customer, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_customer(customer_data, key_store) + .await + } +} + +#[async_trait::async_trait] +impl DisputeInterface for KafkaStore { + async fn insert_dispute( + &self, + dispute: storage::DisputeNew, + ) -> CustomResult { + self.diesel_store.insert_dispute(dispute).await + } + + async fn find_by_merchant_id_payment_id_connector_dispute_id( + &self, + merchant_id: &str, + payment_id: &str, + connector_dispute_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_by_merchant_id_payment_id_connector_dispute_id( + merchant_id, + payment_id, + connector_dispute_id, + ) + .await + } + + async fn find_dispute_by_merchant_id_dispute_id( + &self, + merchant_id: &str, + dispute_id: &str, + ) -> CustomResult { + self.diesel_store + .find_dispute_by_merchant_id_dispute_id(merchant_id, dispute_id) + .await + } + + async fn find_disputes_by_merchant_id( + &self, + merchant_id: &str, + dispute_constraints: api_models::disputes::DisputeListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_disputes_by_merchant_id(merchant_id, dispute_constraints) + .await + } + + async fn update_dispute( + &self, + this: storage::Dispute, + dispute: storage::DisputeUpdate, + ) -> CustomResult { + self.diesel_store.update_dispute(this, dispute).await + } + + async fn find_disputes_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_disputes_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } +} + +#[async_trait::async_trait] +impl EphemeralKeyInterface for KafkaStore { + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyNew, + validity: i64, + ) -> CustomResult { + self.diesel_store.create_ephemeral_key(ek, validity).await + } + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } +} + +#[async_trait::async_trait] +impl EventInterface for KafkaStore { + async fn insert_event( + &self, + event: storage::EventNew, + ) -> CustomResult { + self.diesel_store.insert_event(event).await + } + + async fn update_event( + &self, + event_id: String, + event: storage::EventUpdate, + ) -> CustomResult { + self.diesel_store.update_event(event_id, event).await + } +} + +#[async_trait::async_trait] +impl LockerMockUpInterface for KafkaStore { + async fn find_locker_by_card_id( + &self, + card_id: &str, + ) -> CustomResult { + self.diesel_store.find_locker_by_card_id(card_id).await + } + + async fn insert_locker_mock_up( + &self, + new: storage::LockerMockUpNew, + ) -> CustomResult { + self.diesel_store.insert_locker_mock_up(new).await + } + + async fn delete_locker_mock_up( + &self, + card_id: &str, + ) -> CustomResult { + self.diesel_store.delete_locker_mock_up(card_id).await + } +} + +#[async_trait::async_trait] +impl MandateInterface for KafkaStore { + async fn find_mandate_by_merchant_id_mandate_id( + &self, + merchant_id: &str, + mandate_id: &str, + ) -> CustomResult { + self.diesel_store + .find_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id) + .await + } + + async fn find_mandate_by_merchant_id_connector_mandate_id( + &self, + merchant_id: &str, + connector_mandate_id: &str, + ) -> CustomResult { + self.diesel_store + .find_mandate_by_merchant_id_connector_mandate_id(merchant_id, connector_mandate_id) + .await + } + + async fn find_mandate_by_merchant_id_customer_id( + &self, + merchant_id: &str, + customer_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_mandate_by_merchant_id_customer_id(merchant_id, customer_id) + .await + } + + async fn update_mandate_by_merchant_id_mandate_id( + &self, + merchant_id: &str, + mandate_id: &str, + mandate: storage::MandateUpdate, + ) -> CustomResult { + self.diesel_store + .update_mandate_by_merchant_id_mandate_id(merchant_id, mandate_id, mandate) + .await + } + + async fn find_mandates_by_merchant_id( + &self, + merchant_id: &str, + mandate_constraints: api_models::mandates::MandateListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_mandates_by_merchant_id(merchant_id, mandate_constraints) + .await + } + + async fn insert_mandate( + &self, + mandate: storage::MandateNew, + ) -> CustomResult { + self.diesel_store.insert_mandate(mandate).await + } +} + +#[async_trait::async_trait] +impl PaymentLinkInterface for KafkaStore { + async fn find_payment_link_by_payment_link_id( + &self, + payment_link_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payment_link_by_payment_link_id(payment_link_id) + .await + } + + async fn insert_payment_link( + &self, + payment_link_object: storage::PaymentLinkNew, + ) -> CustomResult { + self.diesel_store + .insert_payment_link(payment_link_object) + .await + } + + async fn list_payment_link_by_merchant_id( + &self, + merchant_id: &str, + payment_link_constraints: api_models::payments::PaymentLinkListConstraints, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_payment_link_by_merchant_id(merchant_id, payment_link_constraints) + .await + } +} + +#[async_trait::async_trait] +impl MerchantAccountInterface for KafkaStore { + async fn insert_merchant( + &self, + merchant_account: domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_merchant(merchant_account, key_store) + .await + } + + async fn find_merchant_account_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_account_by_merchant_id(merchant_id, key_store) + .await + } + + async fn update_merchant( + &self, + this: domain::MerchantAccount, + merchant_account: storage::MerchantAccountUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_merchant(this, merchant_account, key_store) + .await + } + + async fn update_specific_fields_in_merchant( + &self, + merchant_id: &str, + merchant_account: storage::MerchantAccountUpdate, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_specific_fields_in_merchant(merchant_id, merchant_account, key_store) + .await + } + + async fn find_merchant_account_by_publishable_key( + &self, + publishable_key: &str, + ) -> CustomResult { + self.diesel_store + .find_merchant_account_by_publishable_key(publishable_key) + .await + } + + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + organization_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_merchant_accounts_by_organization_id(organization_id) + .await + } + + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_account_by_merchant_id(merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ConnectorAccessToken for KafkaStore { + async fn get_access_token( + &self, + merchant_id: &str, + connector_name: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .get_access_token(merchant_id, connector_name) + .await + } + + async fn set_access_token( + &self, + merchant_id: &str, + connector_name: &str, + access_token: AccessToken, + ) -> CustomResult<(), errors::StorageError> { + self.diesel_store + .set_access_token(merchant_id, connector_name, access_token) + .await + } +} + +#[async_trait::async_trait] +impl FileMetadataInterface for KafkaStore { + async fn insert_file_metadata( + &self, + file: storage::FileMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_file_metadata(file).await + } + + async fn find_file_metadata_by_merchant_id_file_id( + &self, + merchant_id: &str, + file_id: &str, + ) -> CustomResult { + self.diesel_store + .find_file_metadata_by_merchant_id_file_id(merchant_id, file_id) + .await + } + + async fn delete_file_metadata_by_merchant_id_file_id( + &self, + merchant_id: &str, + file_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_file_metadata_by_merchant_id_file_id(merchant_id, file_id) + .await + } + + async fn update_file_metadata( + &self, + this: storage::FileMetadata, + file_metadata: storage::FileMetadataUpdate, + ) -> CustomResult { + self.diesel_store + .update_file_metadata(this, file_metadata) + .await + } +} + +#[async_trait::async_trait] +impl MerchantConnectorAccountInterface for KafkaStore { + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + merchant_id: &str, + connector: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_connector_label( + merchant_id, + connector, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + merchant_id: &str, + connector_name: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + profile_id: &str, + connector_name: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_merchant_connector_account_by_profile_id_connector_name( + profile_id, + connector_name, + key_store, + ) + .await + } + + async fn insert_merchant_connector_account( + &self, + t: domain::MerchantConnectorAccount, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .insert_merchant_connector_account(t, key_store) + .await + } + + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + merchant_id: &str, + merchant_connector_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + key_store, + ) + .await + } + + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + merchant_id: &str, + get_disabled: bool, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + get_disabled, + key_store, + ) + .await + } + + async fn update_merchant_connector_account( + &self, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + self.diesel_store + .update_merchant_connector_account(this, merchant_connector_account, key_store) + .await + } + + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &str, + merchant_connector_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + ) + .await + } +} + +#[async_trait::async_trait] +impl QueueInterface for KafkaStore { + async fn fetch_consumer_tasks( + &self, + stream_name: &str, + group_name: &str, + consumer_name: &str, + ) -> CustomResult, ProcessTrackerError> { + self.diesel_store + .fetch_consumer_tasks(stream_name, group_name, consumer_name) + .await + } + + async fn consumer_group_create( + &self, + stream: &str, + group: &str, + id: &RedisEntryId, + ) -> CustomResult<(), RedisError> { + self.diesel_store + .consumer_group_create(stream, group, id) + .await + } + + async fn acquire_pt_lock( + &self, + tag: &str, + lock_key: &str, + lock_val: &str, + ttl: i64, + ) -> CustomResult { + self.diesel_store + .acquire_pt_lock(tag, lock_key, lock_val, ttl) + .await + } + + async fn release_pt_lock(&self, tag: &str, lock_key: &str) -> CustomResult { + self.diesel_store.release_pt_lock(tag, lock_key).await + } + + async fn stream_append_entry( + &self, + stream: &str, + entry_id: &RedisEntryId, + fields: Vec<(&str, String)>, + ) -> CustomResult<(), RedisError> { + self.diesel_store + .stream_append_entry(stream, entry_id, fields) + .await + } + + async fn get_key(&self, key: &str) -> CustomResult, RedisError> { + self.diesel_store.get_key(key).await + } +} + +#[async_trait::async_trait] +impl PaymentAttemptInterface for KafkaStore { + async fn insert_payment_attempt( + &self, + payment_attempt: storage::PaymentAttemptNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let attempt = self + .diesel_store + .insert_payment_attempt(payment_attempt, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_attempt(&attempt, None) + .await + { + logger::error!(message="Failed to log analytics event for payment attempt {attempt:?}", error_message=?er) + } + + Ok(attempt) + } + + async fn update_payment_attempt_with_attempt_id( + &self, + this: storage::PaymentAttempt, + payment_attempt: storage::PaymentAttemptUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let attempt = self + .diesel_store + .update_payment_attempt_with_attempt_id(this.clone(), payment_attempt, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_attempt(&attempt, Some(this)) + .await + { + logger::error!(message="Failed to log analytics event for payment attempt {attempt:?}", error_message=?er) + } + + Ok(attempt) + } + + async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + &self, + connector_transaction_id: &str, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_merchant_id_connector_txn_id( + &self, + merchant_id: &str, + connector_txn_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &self, + payment_id: &str, + merchant_id: &str, + attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_id, + merchant_id, + attempt_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_attempt_id_merchant_id( + &self, + attempt_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_attempt_id_merchant_id(attempt_id, merchant_id, storage_scheme) + .await + } + + async fn find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id_merchant_id( + payment_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_payment_attempt_by_preprocessing_id_merchant_id( + &self, + preprocessing_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn get_filters_for_payments( + &self, + pi: &[data_models::payments::PaymentIntent], + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult< + data_models::payments::payment_attempt::PaymentListFilters, + errors::DataStorageError, + > { + self.diesel_store + .get_filters_for_payments(pi, merchant_id, storage_scheme) + .await + } + + async fn get_total_count_of_filtered_payment_attempts( + &self, + merchant_id: &str, + active_attempt_ids: &[String], + connector: Option>, + payment_method: Option>, + payment_method_type: Option>, + authentication_type: Option>, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_filtered_payment_attempts( + merchant_id, + active_attempt_ids, + connector, + payment_method, + payment_method_type, + authentication_type, + storage_scheme, + ) + .await + } + + async fn find_attempts_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .find_attempts_by_merchant_id_payment_id(merchant_id, payment_id, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl PaymentIntentInterface for KafkaStore { + async fn update_payment_intent( + &self, + this: storage::PaymentIntent, + payment_intent: storage::PaymentIntentUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let intent = self + .diesel_store + .update_payment_intent(this.clone(), payment_intent, storage_scheme) + .await?; + + if let Err(er) = self + .kafka_producer + .log_payment_intent(&intent, Some(this)) + .await + { + logger::error!(message="Failed to add analytics entry for Payment Intent {intent:?}", error_message=?er); + }; + + Ok(intent) + } + + async fn insert_payment_intent( + &self, + new: storage::PaymentIntentNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + logger::debug!("Inserting PaymentIntent Via KafkaStore"); + let intent = self + .diesel_store + .insert_payment_intent(new, storage_scheme) + .await?; + + if let Err(er) = self.kafka_producer.log_payment_intent(&intent, None).await { + logger::error!(message="Failed to add analytics entry for Payment Intent {intent:?}", error_message=?er); + }; + + Ok(intent) + } + + async fn find_payment_intent_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payment_intent_by_payment_id_merchant_id(payment_id, merchant_id, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn filter_payment_intent_by_constraints( + &self, + merchant_id: &str, + filters: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .filter_payment_intent_by_constraints(merchant_id, filters, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn filter_payment_intents_by_time_range_constraints( + &self, + merchant_id: &str, + time_range: &api_models::payments::TimeRange, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .filter_payment_intents_by_time_range_constraints( + merchant_id, + time_range, + storage_scheme, + ) + .await + } + + #[cfg(feature = "olap")] + async fn get_filtered_payment_intents_attempt( + &self, + merchant_id: &str, + constraints: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult< + Vec<( + data_models::payments::PaymentIntent, + data_models::payments::payment_attempt::PaymentAttempt, + )>, + errors::DataStorageError, + > { + self.diesel_store + .get_filtered_payment_intents_attempt(merchant_id, constraints, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn get_filtered_active_attempt_ids_for_total_count( + &self, + merchant_id: &str, + constraints: &data_models::payments::payment_intent::PaymentIntentFetchConstraints, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .get_filtered_active_attempt_ids_for_total_count( + merchant_id, + constraints, + storage_scheme, + ) + .await + } + + async fn get_active_payment_attempt( + &self, + payment: &mut storage::PaymentIntent, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + self.diesel_store + .get_active_payment_attempt(payment, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl PaymentMethodInterface for KafkaStore { + async fn find_payment_method( + &self, + payment_method_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payment_method(payment_method_id) + .await + } + + async fn find_payment_method_by_customer_id_merchant_id_list( + &self, + customer_id: &str, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id) + .await + } + + async fn insert_payment_method( + &self, + m: storage::PaymentMethodNew, + ) -> CustomResult { + self.diesel_store.insert_payment_method(m).await + } + + async fn update_payment_method( + &self, + payment_method: storage::PaymentMethod, + payment_method_update: storage::PaymentMethodUpdate, + ) -> CustomResult { + self.diesel_store + .update_payment_method(payment_method, payment_method_update) + .await + } + + async fn delete_payment_method_by_merchant_id_payment_method_id( + &self, + merchant_id: &str, + payment_method_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_payment_method_by_merchant_id_payment_method_id(merchant_id, payment_method_id) + .await + } +} + +#[async_trait::async_trait] +impl PayoutAttemptInterface for KafkaStore { + async fn find_payout_attempt_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id) + .await + } + + async fn update_payout_attempt_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + payout: storage::PayoutAttemptUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .await + } + + async fn insert_payout_attempt( + &self, + payout: storage::PayoutAttemptNew, + ) -> CustomResult { + self.diesel_store.insert_payout_attempt(payout).await + } +} + +#[async_trait::async_trait] +impl PayoutsInterface for KafkaStore { + async fn find_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + ) -> CustomResult { + self.diesel_store + .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) + .await + } + + async fn update_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + payout: storage::PayoutsUpdate, + ) -> CustomResult { + self.diesel_store + .update_payout_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .await + } + + async fn insert_payout( + &self, + payout: storage::PayoutsNew, + ) -> CustomResult { + self.diesel_store.insert_payout(payout).await + } +} + +#[async_trait::async_trait] +impl ProcessTrackerInterface for KafkaStore { + async fn reinitialize_limbo_processes( + &self, + ids: Vec, + schedule_time: PrimitiveDateTime, + ) -> CustomResult { + self.diesel_store + .reinitialize_limbo_processes(ids, schedule_time) + .await + } + + async fn find_process_by_id( + &self, + id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.find_process_by_id(id).await + } + + async fn update_process( + &self, + this: storage::ProcessTracker, + process: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store.update_process(this, process).await + } + + async fn process_tracker_update_process_status_by_ids( + &self, + task_ids: Vec, + task_update: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store + .process_tracker_update_process_status_by_ids(task_ids, task_update) + .await + } + async fn update_process_tracker( + &self, + this: storage::ProcessTracker, + process: storage::ProcessTrackerUpdate, + ) -> CustomResult { + self.diesel_store + .update_process_tracker(this, process) + .await + } + + async fn insert_process( + &self, + new: storage::ProcessTrackerNew, + ) -> CustomResult { + self.diesel_store.insert_process(new).await + } + + async fn find_processes_by_time_status( + &self, + time_lower_limit: PrimitiveDateTime, + time_upper_limit: PrimitiveDateTime, + status: ProcessTrackerStatus, + limit: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_processes_by_time_status(time_lower_limit, time_upper_limit, status, limit) + .await + } +} + +#[async_trait::async_trait] +impl CaptureInterface for KafkaStore { + async fn insert_capture( + &self, + capture: storage::CaptureNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_capture(capture, storage_scheme) + .await + } + + async fn update_capture_with_capture_id( + &self, + this: storage::Capture, + capture: storage::CaptureUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_capture_with_capture_id(this, capture, storage_scheme) + .await + } + + async fn find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + &self, + merchant_id: &str, + payment_id: &str, + authorized_attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_captures_by_merchant_id_payment_id_authorized_attempt_id( + merchant_id, + payment_id, + authorized_attempt_id, + storage_scheme, + ) + .await + } +} + +#[async_trait::async_trait] +impl RefundInterface for KafkaStore { + async fn find_refund_by_internal_reference_id_merchant_id( + &self, + internal_reference_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_internal_reference_id_merchant_id( + internal_reference_id, + merchant_id, + storage_scheme, + ) + .await + } + + async fn find_refund_by_payment_id_merchant_id( + &self, + payment_id: &str, + merchant_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_refund_by_payment_id_merchant_id(payment_id, merchant_id, storage_scheme) + .await + } + + async fn find_refund_by_merchant_id_refund_id( + &self, + merchant_id: &str, + refund_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_merchant_id_refund_id(merchant_id, refund_id, storage_scheme) + .await + } + + async fn find_refund_by_merchant_id_connector_refund_id_connector( + &self, + merchant_id: &str, + connector_refund_id: &str, + connector: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_refund_by_merchant_id_connector_refund_id_connector( + merchant_id, + connector_refund_id, + connector, + storage_scheme, + ) + .await + } + + async fn update_refund( + &self, + this: storage::Refund, + refund: storage::RefundUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let refund = self + .diesel_store + .update_refund(this.clone(), refund, storage_scheme) + .await?; + + if let Err(er) = self.kafka_producer.log_refund(&refund, Some(this)).await { + logger::error!(message="Failed to insert analytics event for Refund Update {refund?}", error_message=?er); + } + Ok(refund) + } + + async fn find_refund_by_merchant_id_connector_transaction_id( + &self, + merchant_id: &str, + connector_transaction_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_refund_by_merchant_id_connector_transaction_id( + merchant_id, + connector_transaction_id, + storage_scheme, + ) + .await + } + + async fn insert_refund( + &self, + new: storage::RefundNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let refund = self.diesel_store.insert_refund(new, storage_scheme).await?; + + if let Err(er) = self.kafka_producer.log_refund(&refund, None).await { + logger::error!(message="Failed to insert analytics event for Refund Create {refund?}", error_message=?er); + } + Ok(refund) + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: MerchantStorageScheme, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .filter_refund_by_constraints( + merchant_id, + refund_details, + storage_scheme, + limit, + offset, + ) + .await + } + + #[cfg(feature = "olap")] + async fn filter_refund_by_meta_constraints( + &self, + merchant_id: &str, + refund_details: &api_models::payments::TimeRange, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .filter_refund_by_meta_constraints(merchant_id, refund_details, storage_scheme) + .await + } + + #[cfg(feature = "olap")] + async fn get_total_count_of_refunds( + &self, + merchant_id: &str, + refund_details: &api_models::refunds::RefundListRequest, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_total_count_of_refunds(merchant_id, refund_details, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl MerchantKeyStoreInterface for KafkaStore { + async fn insert_merchant_key_store( + &self, + merchant_key_store: domain::MerchantKeyStore, + key: &Secret>, + ) -> CustomResult { + self.diesel_store + .insert_merchant_key_store(merchant_key_store, key) + .await + } + + async fn get_merchant_key_store_by_merchant_id( + &self, + merchant_id: &str, + key: &Secret>, + ) -> CustomResult { + self.diesel_store + .get_merchant_key_store_by_merchant_id(merchant_id, key) + .await + } + + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_merchant_key_store_by_merchant_id(merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl BusinessProfileInterface for KafkaStore { + async fn insert_business_profile( + &self, + business_profile: business_profile::BusinessProfileNew, + ) -> CustomResult { + self.diesel_store + .insert_business_profile(business_profile) + .await + } + + async fn find_business_profile_by_profile_id( + &self, + profile_id: &str, + ) -> CustomResult { + self.diesel_store + .find_business_profile_by_profile_id(profile_id) + .await + } + + async fn update_business_profile_by_profile_id( + &self, + current_state: business_profile::BusinessProfile, + business_profile_update: business_profile::BusinessProfileUpdateInternal, + ) -> CustomResult { + self.diesel_store + .update_business_profile_by_profile_id(current_state, business_profile_update) + .await + } + + async fn delete_business_profile_by_profile_id_merchant_id( + &self, + profile_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_business_profile_by_profile_id_merchant_id(profile_id, merchant_id) + .await + } + + async fn list_business_profile_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_business_profile_by_merchant_id(merchant_id) + .await + } + + async fn find_business_profile_by_profile_name_merchant_id( + &self, + profile_name: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_business_profile_by_profile_name_merchant_id(profile_name, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ReverseLookupInterface for KafkaStore { + async fn insert_reverse_lookup( + &self, + new: ReverseLookupNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_reverse_lookup(new, storage_scheme) + .await + } + + async fn get_lookup_by_lookup_id( + &self, + id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .get_lookup_by_lookup_id(id, storage_scheme) + .await + } +} + +#[async_trait::async_trait] +impl RoutingAlgorithmInterface for KafkaStore { + async fn insert_routing_algorithm( + &self, + routing_algorithm: storage::RoutingAlgorithm, + ) -> CustomResult { + self.diesel_store + .insert_routing_algorithm(routing_algorithm) + .await + } + + async fn find_routing_algorithm_by_profile_id_algorithm_id( + &self, + profile_id: &str, + algorithm_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_by_profile_id_algorithm_id(profile_id, algorithm_id) + .await + } + + async fn find_routing_algorithm_by_algorithm_id_merchant_id( + &self, + algorithm_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_by_algorithm_id_merchant_id(algorithm_id, merchant_id) + .await + } + + async fn find_routing_algorithm_metadata_by_algorithm_id_profile_id( + &self, + algorithm_id: &str, + profile_id: &str, + ) -> CustomResult { + self.diesel_store + .find_routing_algorithm_metadata_by_algorithm_id_profile_id(algorithm_id, profile_id) + .await + } + + async fn list_routing_algorithm_metadata_by_profile_id( + &self, + profile_id: &str, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_profile_id(profile_id, limit, offset) + .await + } + + async fn list_routing_algorithm_metadata_by_merchant_id( + &self, + merchant_id: &str, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_merchant_id(merchant_id, limit, offset) + .await + } +} + +#[async_trait::async_trait] +impl GsmInterface for KafkaStore { + async fn add_gsm_rule( + &self, + rule: storage::GatewayStatusMappingNew, + ) -> CustomResult { + self.diesel_store.add_gsm_rule(rule).await + } + + async fn find_gsm_decision( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .find_gsm_decision(connector, flow, sub_flow, code, message) + .await + } + + async fn find_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .find_gsm_rule(connector, flow, sub_flow, code, message) + .await + } + + async fn update_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + data: storage::GatewayStatusMappingUpdate, + ) -> CustomResult { + self.diesel_store + .update_gsm_rule(connector, flow, sub_flow, code, message, data) + .await + } + + async fn delete_gsm_rule( + &self, + connector: String, + flow: String, + sub_flow: String, + code: String, + message: String, + ) -> CustomResult { + self.diesel_store + .delete_gsm_rule(connector, flow, sub_flow, code, message) + .await + } +} + +#[async_trait::async_trait] +impl StorageInterface for KafkaStore { + fn get_scheduler_db(&self) -> Box { + Box::new(self.clone()) + } +} + +#[async_trait::async_trait] +impl SchedulerInterface for KafkaStore {} + +impl MasterKeyInterface for KafkaStore { + fn get_master_key(&self) -> &[u8] { + self.diesel_store.get_master_key() + } +} +#[async_trait::async_trait] +impl UserInterface for KafkaStore { + async fn insert_user( + &self, + user_data: storage::UserNew, + ) -> CustomResult { + self.diesel_store.insert_user(user_data).await + } + + async fn find_user_by_email( + &self, + user_email: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_email(user_email).await + } + + async fn find_user_by_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_by_id(user_id).await + } + + async fn update_user_by_user_id( + &self, + user_id: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_user_id(user_id, user) + .await + } + + async fn delete_user_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.delete_user_by_user_id(user_id).await + } +} + +impl RedisConnInterface for KafkaStore { + fn get_redis_conn(&self) -> CustomResult, RedisError> { + self.diesel_store.get_redis_conn() + } +} + +#[async_trait::async_trait] +impl UserRoleInterface for KafkaStore { + async fn insert_user_role( + &self, + user_role: user_storage::UserRoleNew, + ) -> CustomResult { + self.diesel_store.insert_user_role(user_role).await + } + async fn find_user_role_by_user_id( + &self, + user_id: &str, + ) -> CustomResult { + self.diesel_store.find_user_role_by_user_id(user_id).await + } + async fn update_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + update: user_storage::UserRoleUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) + .await + } + async fn delete_user_role(&self, user_id: &str) -> CustomResult { + self.diesel_store.delete_user_role(user_id).await + } + async fn list_user_roles_by_user_id( + &self, + user_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.list_user_roles_by_user_id(user_id).await + } +} diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 39a8543a68c4..8f980fee504a 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -1,15 +1,21 @@ -use serde::Serialize; +use data_models::errors::{StorageError, StorageResult}; +use error_stack::ResultExt; +use serde::{Deserialize, Serialize}; +use storage_impl::errors::ApplicationError; + +use crate::{db::KafkaProducer, services::kafka::KafkaSettings}; pub mod api_logs; pub mod event_logger; +pub mod kafka_handler; -pub trait EventHandler: Sync + Send + dyn_clone::DynClone { +pub(super) trait EventHandler: Sync + Send + dyn_clone::DynClone { fn log_event(&self, event: RawEvent); } dyn_clone::clone_trait_object!(EventHandler); -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct RawEvent { pub event_type: EventType, pub key: String, @@ -24,3 +30,55 @@ pub enum EventType { Refund, ApiLogs, } + +#[derive(Debug, Default, Deserialize, Clone)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum EventsConfig { + Kafka { + kafka: KafkaSettings, + }, + #[default] + Logs, +} + +#[derive(Debug, Clone)] +pub enum EventsHandler { + Kafka(KafkaProducer), + Logs(event_logger::EventLogger), +} + +impl Default for EventsHandler { + fn default() -> Self { + Self::Logs(event_logger::EventLogger {}) + } +} + +impl EventsConfig { + pub async fn get_event_handler(&self) -> StorageResult { + Ok(match self { + Self::Kafka { kafka } => EventsHandler::Kafka( + KafkaProducer::create(kafka) + .await + .change_context(StorageError::InitializationError)?, + ), + Self::Logs => EventsHandler::Logs(event_logger::EventLogger::default()), + }) + } + + pub fn validate(&self) -> Result<(), ApplicationError> { + match self { + Self::Kafka { kafka } => kafka.validate(), + Self::Logs => Ok(()), + } + } +} + +impl EventsHandler { + pub fn log_event(&self, event: RawEvent) { + match self { + Self::Kafka(kafka) => kafka.log_event(event), + Self::Logs(logger) => logger.log_event(event), + } + } +} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 3f598e88394b..bfc10f722c1f 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -24,6 +24,7 @@ use crate::{ #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub struct ApiEvent { + merchant_id: Option, api_flow: String, created_at_timestamp: i128, request_id: String, @@ -40,11 +41,13 @@ pub struct ApiEvent { #[serde(flatten)] event_type: ApiEventsType, hs_latency: Option, + http_method: Option, } impl ApiEvent { #[allow(clippy::too_many_arguments)] pub fn new( + merchant_id: Option, api_flow: &impl FlowMetric, request_id: &RequestId, latency: u128, @@ -56,8 +59,10 @@ impl ApiEvent { error: Option, event_type: ApiEventsType, http_req: &HttpRequest, + http_method: Option, ) -> Self { Self { + merchant_id, api_flow: api_flow.to_string(), created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, request_id: request_id.as_hyphenated().to_string(), @@ -78,6 +83,7 @@ impl ApiEvent { url_path: http_req.path().to_string(), event_type, hs_latency, + http_method, } } } diff --git a/crates/router/src/events/event_logger.rs b/crates/router/src/events/event_logger.rs index fda9b1a036ae..1bd75341be4a 100644 --- a/crates/router/src/events/event_logger.rs +++ b/crates/router/src/events/event_logger.rs @@ -7,6 +7,6 @@ pub struct EventLogger {} impl EventHandler for EventLogger { #[track_caller] fn log_event(&self, event: RawEvent) { - logger::info!(event = ?serde_json::to_string(&event.payload).unwrap_or(r#"{ "error": "Serialization failed" }"#.to_string()), event_type =? event.event_type, event_id =? event.key, log_type = "event"); + logger::info!(event = ?event.payload.to_string(), event_type =? event.event_type, event_id =? event.key, log_type = "event"); } } diff --git a/crates/router/src/events/kafka_handler.rs b/crates/router/src/events/kafka_handler.rs new file mode 100644 index 000000000000..d55847e6e8e7 --- /dev/null +++ b/crates/router/src/events/kafka_handler.rs @@ -0,0 +1,29 @@ +use error_stack::{IntoReport, ResultExt}; +use router_env::tracing; + +use super::{EventHandler, RawEvent}; +use crate::{ + db::MQResult, + services::kafka::{KafkaError, KafkaMessage, KafkaProducer}, +}; +impl EventHandler for KafkaProducer { + fn log_event(&self, event: RawEvent) { + let topic = self.get_topic(event.event_type); + if let Err(er) = self.log_kafka_event(topic, &event) { + tracing::error!("Failed to log event to kafka: {:?}", er); + } + } +} + +impl KafkaMessage for RawEvent { + fn key(&self) -> String { + self.key.clone() + } + + fn value(&self) -> MQResult> { + // Add better error logging here + serde_json::to_vec(&self.payload) + .into_report() + .change_context(KafkaError::GenericError) + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 2b1f9c692d86..035314f71dfb 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,8 +1,6 @@ #![forbid(unsafe_code)] #![recursion_limit = "256"] -#[cfg(feature = "olap")] -pub mod analytics; #[cfg(feature = "stripe")] pub mod compatibility; pub mod configs; @@ -17,6 +15,8 @@ pub(crate) mod macros; pub mod routes; pub mod workflows; +#[cfg(feature = "olap")] +pub mod analytics; pub mod events; pub mod middleware; pub mod openapi; @@ -35,10 +35,7 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; -use crate::{ - configs::settings, - core::errors::{self}, -}; +use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] #[global_allocator] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 1a6f36363d1d..80993429c4e2 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -33,7 +33,7 @@ use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, - events::{event_logger::EventLogger, EventHandler}, + events::EventsHandler, routes::cards_info::card_iin_info, services::get_store, }; @@ -43,7 +43,7 @@ pub struct AppState { pub flow_name: String, pub store: Box, pub conf: Arc, - pub event_handler: Box, + pub event_handler: EventsHandler, #[cfg(feature = "email")] pub email_client: Arc, #[cfg(feature = "kms")] @@ -62,7 +62,7 @@ impl scheduler::SchedulerAppState for AppState { pub trait AppStateInfo { fn conf(&self) -> settings::Settings; fn store(&self) -> Box; - fn event_handler(&self) -> Box; + fn event_handler(&self) -> EventsHandler; #[cfg(feature = "email")] fn email_client(&self) -> Arc; fn add_request_id(&mut self, request_id: RequestId); @@ -82,8 +82,8 @@ impl AppStateInfo for AppState { fn email_client(&self) -> Arc { self.email_client.to_owned() } - fn event_handler(&self) -> Box { - self.event_handler.to_owned() + fn event_handler(&self) -> EventsHandler { + self.event_handler.clone() } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); @@ -130,13 +130,31 @@ impl AppState { #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&conf.kms).await; let testable = storage_impl == StorageImpl::PostgresqlTest; + #[allow(clippy::expect_used)] + let event_handler = conf + .events + .get_event_handler() + .await + .expect("Failed to create event handler"); let store: Box = match storage_impl { - StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( - #[allow(clippy::expect_used)] - get_store(&conf, shut_down_signal, testable) - .await - .expect("Failed to create store"), - ), + StorageImpl::Postgresql | StorageImpl::PostgresqlTest => match &event_handler { + EventsHandler::Kafka(kafka_client) => Box::new( + crate::db::KafkaStore::new( + #[allow(clippy::expect_used)] + get_store(&conf.clone(), shut_down_signal, testable) + .await + .expect("Failed to create store"), + kafka_client.clone(), + ) + .await, + ), + EventsHandler::Logs(_) => Box::new( + #[allow(clippy::expect_used)] + get_store(&conf, shut_down_signal, testable) + .await + .expect("Failed to create store"), + ), + }, #[allow(clippy::expect_used)] StorageImpl::Mock => Box::new( MockDb::new(&conf.redis) @@ -146,12 +164,7 @@ impl AppState { }; #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, - #[cfg(feature = "kms")] - kms_client, - ) - .await; + let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; #[cfg(feature = "kms")] #[allow(clippy::expect_used)] @@ -174,7 +187,7 @@ impl AppState { #[cfg(feature = "kms")] kms_secrets: Arc::new(kms_secrets), api_client, - event_handler: Box::::default(), + event_handler, #[cfg(feature = "olap")] pool, } diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index faea707f2a14..e46612b95dfc 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -4,6 +4,7 @@ pub mod authorization; pub mod encryption; #[cfg(feature = "olap")] pub mod jwt; +pub mod kafka; pub mod logger; #[cfg(feature = "email")] diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 5481d5c5cf9d..1ff46474db59 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -873,6 +873,7 @@ where }; let api_event = ApiEvent::new( + Some(merchant_id.clone()), flow, &request_id, request_duration, @@ -884,6 +885,7 @@ where error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, + Some(request.method().to_string()), ); match api_event.clone().try_into() { Ok(event) => { diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs new file mode 100644 index 000000000000..497ac16721b5 --- /dev/null +++ b/crates/router/src/services/kafka.rs @@ -0,0 +1,314 @@ +use std::sync::Arc; + +use common_utils::errors::CustomResult; +use error_stack::{report, IntoReport, ResultExt}; +use rdkafka::{ + config::FromClientConfig, + producer::{BaseRecord, DefaultProducerContext, Producer, ThreadedProducer}, +}; + +use crate::events::EventType; +mod api_event; +pub mod outgoing_request; +mod payment_attempt; +mod payment_intent; +mod refund; +pub use api_event::{ApiCallEventType, ApiEvents, ApiEventsType}; +use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use diesel_models::refund::Refund; +use serde::Serialize; +use time::OffsetDateTime; + +use self::{ + payment_attempt::KafkaPaymentAttempt, payment_intent::KafkaPaymentIntent, refund::KafkaRefund, +}; +// Using message queue result here to avoid confusion with Kafka result provided by library +pub type MQResult = CustomResult; + +pub trait KafkaMessage +where + Self: Serialize, +{ + fn value(&self) -> MQResult> { + // Add better error logging here + serde_json::to_vec(&self) + .into_report() + .change_context(KafkaError::GenericError) + } + + fn key(&self) -> String; + + fn creation_timestamp(&self) -> Option { + None + } +} + +#[derive(serde::Serialize, Debug)] +struct KafkaEvent<'a, T: KafkaMessage> { + #[serde(flatten)] + event: &'a T, + sign_flag: i32, +} + +impl<'a, T: KafkaMessage> KafkaEvent<'a, T> { + fn new(event: &'a T) -> Self { + Self { + event, + sign_flag: 1, + } + } + fn old(event: &'a T) -> Self { + Self { + event, + sign_flag: -1, + } + } +} + +impl<'a, T: KafkaMessage> KafkaMessage for KafkaEvent<'a, T> { + fn key(&self) -> String { + self.event.key() + } + + fn creation_timestamp(&self) -> Option { + self.event.creation_timestamp() + } +} + +#[derive(Debug, serde::Deserialize, Clone, Default)] +#[serde(default)] +pub struct KafkaSettings { + brokers: Vec, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, +} + +impl KafkaSettings { + pub fn validate(&self) -> Result<(), crate::core::errors::ApplicationError> { + use common_utils::ext_traits::ConfigExt; + + use crate::core::errors::ApplicationError; + + common_utils::fp_utils::when(self.brokers.is_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka brokers must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.intent_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Intent Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.attempt_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Attempt Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.refund_analytics_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Refund Analytics topic must not be empty".into(), + )) + })?; + + common_utils::fp_utils::when(self.api_logs_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka API event Analytics topic must not be empty".into(), + )) + }) + } +} + +#[derive(Clone, Debug)] +pub struct KafkaProducer { + producer: Arc, + intent_analytics_topic: String, + attempt_analytics_topic: String, + refund_analytics_topic: String, + api_logs_topic: String, +} + +struct RdKafkaProducer(ThreadedProducer); + +impl std::fmt::Debug for RdKafkaProducer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("RdKafkaProducer") + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum KafkaError { + #[error("Generic Kafka Error")] + GenericError, + #[error("Kafka not implemented")] + NotImplemented, + #[error("Kafka Initialization Error")] + InitializationError, +} + +#[allow(unused)] +impl KafkaProducer { + pub async fn create(conf: &KafkaSettings) -> MQResult { + Ok(Self { + producer: Arc::new(RdKafkaProducer( + ThreadedProducer::from_config( + rdkafka::ClientConfig::new().set("bootstrap.servers", conf.brokers.join(",")), + ) + .into_report() + .change_context(KafkaError::InitializationError)?, + )), + + intent_analytics_topic: conf.intent_analytics_topic.clone(), + attempt_analytics_topic: conf.attempt_analytics_topic.clone(), + refund_analytics_topic: conf.refund_analytics_topic.clone(), + api_logs_topic: conf.api_logs_topic.clone(), + }) + } + + pub fn log_kafka_event( + &self, + topic: &str, + event: &T, + ) -> MQResult<()> { + router_env::logger::debug!("Logging Kafka Event {event:?}"); + self.producer + .0 + .send( + BaseRecord::to(topic) + .key(&event.key()) + .payload(&event.value()?) + .timestamp( + event + .creation_timestamp() + .unwrap_or_else(|| OffsetDateTime::now_utc().unix_timestamp()), + ), + ) + .map_err(|(error, record)| report!(error).attach_printable(format!("{record:?}"))) + .change_context(KafkaError::GenericError) + } + + pub async fn log_payment_attempt( + &self, + attempt: &PaymentAttempt, + old_attempt: Option, + ) -> MQResult<()> { + if let Some(negative_event) = old_attempt { + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::old(&KafkaPaymentAttempt::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative attempt event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::new(&KafkaPaymentAttempt::from_storage(attempt)), + ) + .attach_printable_lazy(|| format!("Failed to add positive attempt event {attempt:?}")) + } + + pub async fn log_payment_attempt_delete( + &self, + delete_old_attempt: &PaymentAttempt, + ) -> MQResult<()> { + self.log_kafka_event( + &self.attempt_analytics_topic, + &KafkaEvent::old(&KafkaPaymentAttempt::from_storage(delete_old_attempt)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative attempt event {delete_old_attempt:?}") + }) + } + + pub async fn log_payment_intent( + &self, + intent: &PaymentIntent, + old_intent: Option, + ) -> MQResult<()> { + if let Some(negative_event) = old_intent { + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::old(&KafkaPaymentIntent::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative intent event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::new(&KafkaPaymentIntent::from_storage(intent)), + ) + .attach_printable_lazy(|| format!("Failed to add positive intent event {intent:?}")) + } + + pub async fn log_payment_intent_delete( + &self, + delete_old_intent: &PaymentIntent, + ) -> MQResult<()> { + self.log_kafka_event( + &self.intent_analytics_topic, + &KafkaEvent::old(&KafkaPaymentIntent::from_storage(delete_old_intent)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative intent event {delete_old_intent:?}") + }) + } + + pub async fn log_refund(&self, refund: &Refund, old_refund: Option) -> MQResult<()> { + if let Some(negative_event) = old_refund { + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::old(&KafkaRefund::from_storage(&negative_event)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative refund event {negative_event:?}") + })?; + }; + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::new(&KafkaRefund::from_storage(refund)), + ) + .attach_printable_lazy(|| format!("Failed to add positive refund event {refund:?}")) + } + + pub async fn log_refund_delete(&self, delete_old_refund: &Refund) -> MQResult<()> { + self.log_kafka_event( + &self.refund_analytics_topic, + &KafkaEvent::old(&KafkaRefund::from_storage(delete_old_refund)), + ) + .attach_printable_lazy(|| { + format!("Failed to add negative refund event {delete_old_refund:?}") + }) + } + + pub async fn log_api_event(&self, event: &ApiEvents) -> MQResult<()> { + self.log_kafka_event(&self.api_logs_topic, event) + .attach_printable_lazy(|| format!("Failed to add api log event {event:?}")) + } + + pub fn get_topic(&self, event: EventType) -> &str { + match event { + EventType::ApiLogs => &self.api_logs_topic, + EventType::PaymentAttempt => &self.attempt_analytics_topic, + EventType::PaymentIntent => &self.intent_analytics_topic, + EventType::Refund => &self.refund_analytics_topic, + } + } +} + +impl Drop for RdKafkaProducer { + fn drop(&mut self) { + // Flush the producer to send any pending messages + match self.0.flush(rdkafka::util::Timeout::After( + std::time::Duration::from_secs(5), + )) { + Ok(_) => router_env::logger::info!("Kafka events flush Successful"), + Err(error) => router_env::logger::error!("Failed to flush Kafka Events {error:?}"), + } + } +} diff --git a/crates/router/src/services/kafka/api_event.rs b/crates/router/src/services/kafka/api_event.rs new file mode 100644 index 000000000000..7de271915927 --- /dev/null +++ b/crates/router/src/services/kafka/api_event.rs @@ -0,0 +1,108 @@ +use api_models::enums as api_enums; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "flow_type")] +pub enum ApiEventsType { + Payment { + payment_id: String, + }, + Refund { + payment_id: String, + refund_id: String, + }, + Default, + PaymentMethod { + payment_method_id: String, + payment_method: Option, + payment_method_type: Option, + }, + Customer { + customer_id: String, + }, + User { + //specified merchant_id will overridden on global defined + merchant_id: String, + user_id: String, + }, + Webhooks { + connector: String, + payment_id: Option, + }, + OutgoingEvent, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ApiEvents { + pub api_name: String, + pub request_id: Option, + //It is require to solve ambiquity in case of event_type is User + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_id: Option, + pub request: String, + pub response: String, + pub status_code: u16, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + pub latency: u128, + //conflicting fields underlying enums will be used + #[serde(flatten)] + pub event_type: ApiEventsType, + pub user_agent: Option, + pub ip_addr: Option, + pub url_path: Option, + pub api_event_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum ApiCallEventType { + IncomingApiEvent, + OutgoingApiEvent, +} + +impl super::KafkaMessage for ApiEvents { + fn key(&self) -> String { + match &self.event_type { + ApiEventsType::Payment { payment_id } => format!( + "{}_{}", + self.merchant_id + .as_ref() + .unwrap_or(&"default_merchant_id".to_string()), + payment_id + ), + ApiEventsType::Refund { + payment_id, + refund_id, + } => format!("{payment_id}_{refund_id}"), + ApiEventsType::Default => "key".to_string(), + ApiEventsType::PaymentMethod { + payment_method_id, + payment_method, + payment_method_type, + } => format!( + "{:?}_{:?}_{:?}", + payment_method_id.clone(), + payment_method.clone(), + payment_method_type.clone(), + ), + ApiEventsType::Customer { customer_id } => customer_id.to_string(), + ApiEventsType::User { + merchant_id, + user_id, + } => format!("{}_{}", merchant_id, user_id), + ApiEventsType::Webhooks { + connector, + payment_id, + } => format!( + "webhook_{}_{connector}", + payment_id.clone().unwrap_or_default() + ), + ApiEventsType::OutgoingEvent => "outgoing_event".to_string(), + } + } + + fn creation_timestamp(&self) -> Option { + Some(self.created_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/outgoing_request.rs b/crates/router/src/services/kafka/outgoing_request.rs new file mode 100644 index 000000000000..bb09fe91fe6d --- /dev/null +++ b/crates/router/src/services/kafka/outgoing_request.rs @@ -0,0 +1,19 @@ +use reqwest::Url; + +pub struct OutgoingRequest { + pub url: Url, + pub latency: u128, +} + +// impl super::KafkaMessage for OutgoingRequest { +// fn key(&self) -> String { +// format!( +// "{}_{}", + +// ) +// } + +// fn creation_timestamp(&self) -> Option { +// Some(self.created_at.unix_timestamp()) +// } +// } diff --git a/crates/router/src/services/kafka/payment_attempt.rs b/crates/router/src/services/kafka/payment_attempt.rs new file mode 100644 index 000000000000..ea0721f418e5 --- /dev/null +++ b/crates/router/src/services/kafka/payment_attempt.rs @@ -0,0 +1,92 @@ +use data_models::payments::payment_attempt::PaymentAttempt; +use diesel_models::enums as storage_enums; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaPaymentAttempt<'a> { + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub attempt_id: &'a String, + pub status: storage_enums::AttemptStatus, + pub amount: i64, + pub currency: Option, + pub save_to_locker: Option, + pub connector: Option<&'a String>, + pub error_message: Option<&'a String>, + pub offer_amount: Option, + pub surcharge_amount: Option, + pub tax_amount: Option, + pub payment_method_id: Option<&'a String>, + pub payment_method: Option, + pub connector_transaction_id: Option<&'a String>, + pub capture_method: Option, + #[serde(default, with = "time::serde::timestamp::option")] + pub capture_on: Option, + pub confirm: bool, + pub authentication_type: Option, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp::option")] + pub last_synced: Option, + pub cancellation_reason: Option<&'a String>, + pub amount_to_capture: Option, + pub mandate_id: Option<&'a String>, + pub browser_info: Option, + pub error_code: Option<&'a String>, + pub connector_metadata: Option, + // TODO: These types should implement copy ideally + pub payment_experience: Option<&'a storage_enums::PaymentExperience>, + pub payment_method_type: Option<&'a storage_enums::PaymentMethodType>, +} + +impl<'a> KafkaPaymentAttempt<'a> { + pub fn from_storage(attempt: &'a PaymentAttempt) -> Self { + Self { + payment_id: &attempt.payment_id, + merchant_id: &attempt.merchant_id, + attempt_id: &attempt.attempt_id, + status: attempt.status, + amount: attempt.amount, + currency: attempt.currency, + save_to_locker: attempt.save_to_locker, + connector: attempt.connector.as_ref(), + error_message: attempt.error_message.as_ref(), + offer_amount: attempt.offer_amount, + surcharge_amount: attempt.surcharge_amount, + tax_amount: attempt.tax_amount, + payment_method_id: attempt.payment_method_id.as_ref(), + payment_method: attempt.payment_method, + connector_transaction_id: attempt.connector_transaction_id.as_ref(), + capture_method: attempt.capture_method, + capture_on: attempt.capture_on.map(|i| i.assume_utc()), + confirm: attempt.confirm, + authentication_type: attempt.authentication_type, + created_at: attempt.created_at.assume_utc(), + modified_at: attempt.modified_at.assume_utc(), + last_synced: attempt.last_synced.map(|i| i.assume_utc()), + cancellation_reason: attempt.cancellation_reason.as_ref(), + amount_to_capture: attempt.amount_to_capture, + mandate_id: attempt.mandate_id.as_ref(), + browser_info: attempt.browser_info.as_ref().map(|v| v.to_string()), + error_code: attempt.error_code.as_ref(), + connector_metadata: attempt.connector_metadata.as_ref().map(|v| v.to_string()), + payment_experience: attempt.payment_experience.as_ref(), + payment_method_type: attempt.payment_method_type.as_ref(), + } + } +} + +impl<'a> super::KafkaMessage for KafkaPaymentAttempt<'a> { + fn key(&self) -> String { + format!( + "{}_{}_{}", + self.merchant_id, self.payment_id, self.attempt_id + ) + } + + fn creation_timestamp(&self) -> Option { + Some(self.modified_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/payment_intent.rs b/crates/router/src/services/kafka/payment_intent.rs new file mode 100644 index 000000000000..70980a6e8652 --- /dev/null +++ b/crates/router/src/services/kafka/payment_intent.rs @@ -0,0 +1,71 @@ +use data_models::payments::PaymentIntent; +use diesel_models::enums as storage_enums; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaPaymentIntent<'a> { + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub status: storage_enums::IntentStatus, + pub amount: i64, + pub currency: Option, + pub amount_captured: Option, + pub customer_id: Option<&'a String>, + pub description: Option<&'a String>, + pub return_url: Option<&'a String>, + pub connector_id: Option<&'a String>, + pub statement_descriptor_name: Option<&'a String>, + pub statement_descriptor_suffix: Option<&'a String>, + #[serde(with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp::option")] + pub last_synced: Option, + pub setup_future_usage: Option, + pub off_session: Option, + pub client_secret: Option<&'a String>, + pub active_attempt_id: String, + pub business_country: Option, + pub business_label: Option<&'a String>, + pub attempt_count: i16, +} + +impl<'a> KafkaPaymentIntent<'a> { + pub fn from_storage(intent: &'a PaymentIntent) -> Self { + Self { + payment_id: &intent.payment_id, + merchant_id: &intent.merchant_id, + status: intent.status, + amount: intent.amount, + currency: intent.currency, + amount_captured: intent.amount_captured, + customer_id: intent.customer_id.as_ref(), + description: intent.description.as_ref(), + return_url: intent.return_url.as_ref(), + connector_id: intent.connector_id.as_ref(), + statement_descriptor_name: intent.statement_descriptor_name.as_ref(), + statement_descriptor_suffix: intent.statement_descriptor_suffix.as_ref(), + created_at: intent.created_at.assume_utc(), + modified_at: intent.modified_at.assume_utc(), + last_synced: intent.last_synced.map(|i| i.assume_utc()), + setup_future_usage: intent.setup_future_usage, + off_session: intent.off_session, + client_secret: intent.client_secret.as_ref(), + active_attempt_id: intent.active_attempt.get_id(), + business_country: intent.business_country, + business_label: intent.business_label.as_ref(), + attempt_count: intent.attempt_count, + } + } +} + +impl<'a> super::KafkaMessage for KafkaPaymentIntent<'a> { + fn key(&self) -> String { + format!("{}_{}", self.merchant_id, self.payment_id) + } + + fn creation_timestamp(&self) -> Option { + Some(self.modified_at.unix_timestamp()) + } +} diff --git a/crates/router/src/services/kafka/refund.rs b/crates/router/src/services/kafka/refund.rs new file mode 100644 index 000000000000..0cc4865e7512 --- /dev/null +++ b/crates/router/src/services/kafka/refund.rs @@ -0,0 +1,68 @@ +use diesel_models::{enums as storage_enums, refund::Refund}; +use time::OffsetDateTime; + +#[derive(serde::Serialize, Debug)] +pub struct KafkaRefund<'a> { + pub internal_reference_id: &'a String, + pub refund_id: &'a String, //merchant_reference id + pub payment_id: &'a String, + pub merchant_id: &'a String, + pub connector_transaction_id: &'a String, + pub connector: &'a String, + pub connector_refund_id: Option<&'a String>, + pub external_reference_id: Option<&'a String>, + pub refund_type: &'a storage_enums::RefundType, + pub total_amount: &'a i64, + pub currency: &'a storage_enums::Currency, + pub refund_amount: &'a i64, + pub refund_status: &'a storage_enums::RefundStatus, + pub sent_to_gateway: &'a bool, + pub refund_error_message: Option<&'a String>, + pub refund_arn: Option<&'a String>, + #[serde(default, with = "time::serde::timestamp")] + pub created_at: OffsetDateTime, + #[serde(default, with = "time::serde::timestamp")] + pub modified_at: OffsetDateTime, + pub description: Option<&'a String>, + pub attempt_id: &'a String, + pub refund_reason: Option<&'a String>, + pub refund_error_code: Option<&'a String>, +} + +impl<'a> KafkaRefund<'a> { + pub fn from_storage(refund: &'a Refund) -> Self { + Self { + internal_reference_id: &refund.internal_reference_id, + refund_id: &refund.refund_id, + payment_id: &refund.payment_id, + merchant_id: &refund.merchant_id, + connector_transaction_id: &refund.connector_transaction_id, + connector: &refund.connector, + connector_refund_id: refund.connector_refund_id.as_ref(), + external_reference_id: refund.external_reference_id.as_ref(), + refund_type: &refund.refund_type, + total_amount: &refund.total_amount, + currency: &refund.currency, + refund_amount: &refund.refund_amount, + refund_status: &refund.refund_status, + sent_to_gateway: &refund.sent_to_gateway, + refund_error_message: refund.refund_error_message.as_ref(), + refund_arn: refund.refund_arn.as_ref(), + created_at: refund.created_at.assume_utc(), + modified_at: refund.updated_at.assume_utc(), + description: refund.description.as_ref(), + attempt_id: &refund.attempt_id, + refund_reason: refund.refund_reason.as_ref(), + refund_error_code: refund.refund_error_code.as_ref(), + } + } +} + +impl<'a> super::KafkaMessage for KafkaRefund<'a> { + fn key(&self) -> String { + format!( + "{}_{}_{}_{}", + self.merchant_id, self.payment_id, self.attempt_id, self.refund_id + ) + } +} diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index f94d06997ca9..13b9f3dd5d5c 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -7,7 +7,6 @@ use error_stack::ResultExt; use crate::{ core::errors, errors::RouterResult, types::transformers::ForeignFrom, utils::OptionExt, }; - pub trait PaymentAttemptExt { fn make_new_capture( &self, @@ -134,9 +133,7 @@ mod tests { use crate::configs::settings::Settings; let conf = Settings::new().expect("invalid settings"); let tx: oneshot::Sender<()> = oneshot::channel().0; - let api_client = Box::new(services::MockApiClient); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx, api_client).await; @@ -187,7 +184,6 @@ mod tests { let tx: oneshot::Sender<()> = oneshot::channel().0; let api_client = Box::new(services::MockApiClient); - let state = routes::AppState::with_storage(conf, StorageImpl::PostgresqlTest, tx, api_client).await; let current_time = common_utils::date_time::now(); diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index c9ee3a34f2ef..e12e27708f87 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -160,6 +160,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { async fn payments_create_success() { let conf = Settings::new().unwrap(); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -204,6 +205,7 @@ async fn payments_create_failure() { let conf = Settings::new().unwrap(); static CV: aci::Aci = aci::Aci; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -265,6 +267,7 @@ async fn refund_for_successful_payments() { merchant_connector_id: None, }; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, @@ -333,6 +336,7 @@ async fn refunds_create_failure() { merchant_connector_id: None, }; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 67a0625968fb..f325370e737f 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -96,6 +96,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -120,6 +121,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -148,6 +150,7 @@ pub trait ConnectorActions: Connector { payment_info, ); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -561,6 +564,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(None, payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -601,6 +605,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(connector_payout_id, payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -642,6 +647,7 @@ pub trait ConnectorActions: Connector { let mut request = self.get_payout_request(None, payout_type, payment_info); request.connector_customer = connector_customer; let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -683,6 +689,7 @@ pub trait ConnectorActions: Connector { let mut request = self.get_payout_request(Some(connector_payout_id), payout_type, payment_info); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -770,6 +777,7 @@ pub trait ConnectorActions: Connector { .get_connector_integration(); let mut request = self.get_payout_request(None, payout_type, payment_info); let tx = oneshot::channel().0; + let state = routes::AppState::with_storage( Settings::new().unwrap(), StorageImpl::PostgresqlTest, @@ -802,6 +810,7 @@ async fn call_connector< ) -> Result, Report> { let conf = Settings::new().unwrap(); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 5d4ca844061f..42e5524a15d5 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -217,6 +217,7 @@ async fn payments_create_core_adyen_no_redirect() { use router::configs::settings::Settings; let conf = Settings::new().expect("invalid settings"); let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( conf, StorageImpl::PostgresqlTest, diff --git a/crates/router/tests/utils.rs b/crates/router/tests/utils.rs index 6cddbc043662..339eca6fa0fb 100644 --- a/crates/router/tests/utils.rs +++ b/crates/router/tests/utils.rs @@ -48,6 +48,7 @@ pub async fn mk_service( conf.connectors.stripe.base_url = url; } let tx: oneshot::Sender<()> = oneshot::channel().0; + let app_state = AppState::with_storage( conf, router::db::StorageImpl::Mock, diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index e75606aa1531..3c7ba8b93df7 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -39,10 +39,19 @@ use crate::types::FlowMetric; #[derive(Debug, Display, Clone, PartialEq, Eq)] pub enum AnalyticsFlow { GetInfo, + GetPaymentMetrics, + GetRefundsMetrics, + GetSdkMetrics, GetPaymentFilters, GetRefundFilters, - GetRefundsMetrics, - GetPaymentMetrics, + GetSdkEventFilters, + GetApiEvents, + GetSdkEvents, + GeneratePaymentReport, + GenerateDisputeReport, + GenerateRefundReport, + GetApiEventMetrics, + GetApiEventFilters, } impl FlowMetric for AnalyticsFlow {} diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index e0b68c709e8d..5e8674ab3814 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [features] default = ["kv_store", "olap"] -olap = [] +olap = ["storage_impl/olap"] kv_store = [] [dependencies] diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index f53507831b11..fd95a6d315d6 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -1,6 +1,6 @@ use masking::Secret; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Deserialize)] pub struct Database { pub username: String, pub password: Secret, @@ -9,7 +9,41 @@ pub struct Database { pub dbname: String, pub pool_size: u32, pub connection_timeout: u64, - pub queue_strategy: bb8::QueueStrategy, + pub queue_strategy: QueueStrategy, pub min_idle: Option, pub max_lifetime: Option, } + +#[derive(Debug, serde::Deserialize, Clone, Copy, Default)] +#[serde(rename_all = "PascalCase")] +pub enum QueueStrategy { + #[default] + Fifo, + Lifo, +} + +impl From for bb8::QueueStrategy { + fn from(value: QueueStrategy) -> Self { + match value { + QueueStrategy::Fifo => Self::Fifo, + QueueStrategy::Lifo => Self::Lifo, + } + } +} + +impl Default for Database { + fn default() -> Self { + Self { + username: String::new(), + password: Secret::::default(), + host: "localhost".into(), + port: 5432, + dbname: String::new(), + pool_size: 5, + connection_timeout: 10, + queue_strategy: QueueStrategy::default(), + min_idle: None, + max_lifetime: None, + } + } +} diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index c36575e37c97..75c34af14ac1 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -89,7 +89,7 @@ pub async fn diesel_make_pg_pool( let mut pool = bb8::Pool::builder() .max_size(database.pool_size) .min_idle(database.min_idle) - .queue_strategy(database.queue_strategy) + .queue_strategy(database.queue_strategy.into()) .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)) .max_lifetime(database.max_lifetime.map(std::time::Duration::from_secs)); diff --git a/docker-compose.yml b/docker-compose.yml index fd18906143f5..f51a47aee940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -273,3 +273,66 @@ services: - "8001:8001" volumes: - redisinsight_store:/db + + kafka0: + image: confluentinc/cp-kafka:7.0.5 + hostname: kafka0 + networks: + - router_net + ports: + - 9092:9092 + - 9093 + - 9997 + - 29092 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093' + KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs' + JMX_PORT: 9997 + KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 + profiles: + - analytics + volumes: + - ./monitoring/kafka-script.sh:/tmp/update_run.sh + command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + + # Kafka UI for debugging kafka queues + kafka-ui: + image: provectuslabs/kafka-ui:latest + ports: + - 8090:8080 + networks: + - router_net + depends_on: + - kafka0 + profiles: + - analytics + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092 + KAFKA_CLUSTERS_0_JMXPORT: 9997 + + clickhouse-server: + image: clickhouse/clickhouse-server:23.5 + networks: + - router_net + ports: + - "9000" + - "8123:8123" + profiles: + - analytics + ulimits: + nofile: + soft: 262144 + hard: 262144 \ No newline at end of file From 70ba4ffe7bb9e685f3dc8afc26de241f2457e86c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:31:26 +0000 Subject: [PATCH 103/443] chore(version): v1.92.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a63dcc2cae0..f2966b238bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.92.0 (2023-11-29) + +### Features + +- **analytics:** Add Clickhouse based analytics ([#2988](https://github.com/juspay/hyperswitch/pull/2988)) ([`9df4e01`](https://github.com/juspay/hyperswitch/commit/9df4e0193ffeb6d1cc323bdebb7e2bdfb2a375e2)) +- **ses_email:** Add email services to hyperswitch ([#2977](https://github.com/juspay/hyperswitch/pull/2977)) ([`5f5e895`](https://github.com/juspay/hyperswitch/commit/5f5e895f638701a0e6ab3deea9101ef39033dd16)) + +### Bug Fixes + +- **router:** Make use of warning to log errors when apple pay metadata parsing fails ([#3010](https://github.com/juspay/hyperswitch/pull/3010)) ([`2e57745`](https://github.com/juspay/hyperswitch/commit/2e57745352c547323ac2df2554f6bc2dbd6da37f)) + +**Full Changelog:** [`v1.91.1...v1.92.0`](https://github.com/juspay/hyperswitch/compare/v1.91.1...v1.92.0) + +- - - + + ## 1.91.1 (2023-11-29) ### Bug Fixes From 6b7ada1a34450ea3a7fc019375ba462a14ddd6ab Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:35:33 +0530 Subject: [PATCH 104/443] fix(core): Error message on Refund update for `Not Implemented` Case (#3011) --- crates/router/src/core/refunds.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 33435bb0ad96..c43c00b7259c 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -211,7 +211,10 @@ pub async fn trigger_refund_to_gateway( errors::ConnectorError::NotImplemented(message) => { Some(storage::RefundUpdate::ErrorUpdate { refund_status: Some(enums::RefundStatus::Failure), - refund_error_message: Some(message.to_string()), + refund_error_message: Some( + errors::ConnectorError::NotImplemented(message.to_owned()) + .to_string(), + ), refund_error_code: Some("NOT_IMPLEMENTED".to_string()), updated_by: storage_scheme.to_string(), }) From c05432c0bd70f222c2f898ce2cbb47a46364a490 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:01:36 +0530 Subject: [PATCH 105/443] fix(pm_list): [Trustpay] Update Cards, Bank_redirect - blik pm type required field info for Trustpay (#2999) Co-authored-by: Arjun Karthik --- crates/router/src/configs/defaults.rs | 172 ++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index a92e63d67639..f5c3b46b27f2 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -1910,14 +1910,63 @@ impl Default for super::settings::RequiredFields { } ), ( - "payment_method_data.card.card_holder_name".to_string(), + "billing.address.first_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), + required_field: "billing.address.first_name".to_string(), display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, + field_type: enums::FieldType::UserBillingName, value: None, } - ) + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), common: HashMap::new() @@ -3686,14 +3735,63 @@ impl Default for super::settings::RequiredFields { } ), ( - "payment_method_data.card.card_holder_name".to_string(), + "billing.address.first_name".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), + required_field: "billing.address.first_name".to_string(), display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, + field_type: enums::FieldType::UserBillingName, value: None, } - ) + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ] ), common: HashMap::new() @@ -4056,6 +4154,64 @@ impl Default for super::settings::RequiredFields { value: None, } ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), ]), } ) From 8a4dabc61df3e6012e50f785d93808ca3349be65 Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:07:59 +0300 Subject: [PATCH 106/443] refactor(connector): [Stax] change error message from NotSupported to NotImplemented (#2879) --- .../router/src/connector/stax/transformers.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index bb37bf1fc9e7..5aa0949a09cc 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -63,10 +63,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme item: &StaxRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { if item.router_data.request.currency != enums::Currency::USD { - Err(errors::ConnectorError::NotSupported { - message: item.router_data.request.currency.to_string(), - connector: "Stax", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))? } let total = item.amount; @@ -119,10 +118,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: "SELECTED_PAYMENT_METHOD".to_string(), - connector: "Stax", - })?, + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))?, } } } @@ -270,10 +268,9 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: "SELECTED_PAYMENT_METHOD".to_string(), - connector: "Stax", - })?, + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Stax"), + ))?, } } } From de8e31b70d9b3c11e268cd1deffa71918dc4270d Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:08:30 +0300 Subject: [PATCH 107/443] refactor(connector): [Volt] change error message from NotSupported to NotImplemented (#2878) --- crates/router/src/connector/volt/transformers.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index efed7c797c76..6f4c67dce8a3 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -130,10 +130,9 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::BankRedirectData::Trustly { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Volt", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Volt"), + ) .into()) } }, @@ -150,10 +149,9 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::GiftCard(_) | api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Volt", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Volt"), + ) .into()) } } From ab3dac79b4f138cd1f60a9afc0635dcc137a4a05 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:24:23 +0530 Subject: [PATCH 108/443] refactor(connector): [Adyen] Change country and issuer type to Optional for OpenBankingUk (#2993) Co-authored-by: Arjun Karthik Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- crates/api_models/src/payments.rs | 4 +- .../src/connector/adyen/transformers.rs | 145 ++++++++++++++++-- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index bd4c59211e24..5ecbf795ac56 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1204,10 +1204,10 @@ pub enum BankRedirectData { OpenBankingUk { // Issuer banks #[schema(value_type = BankNames)] - issuer: api_enums::BankNames, + issuer: Option, /// The country for bank payment #[schema(value_type = CountryAlpha2, example = "US")] - country: api_enums::CountryAlpha2, + country: Option, }, Przelewy24 { //Issuer banks diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index cfa601112677..4b3fcc851323 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -879,7 +879,126 @@ impl TryFrom<&api_enums::BankNames> for OpenBankingUKIssuer { api::enums::BankNames::TsbBank => Ok(Self::TsbBank), api::enums::BankNames::TescoBank => Ok(Self::TescoBank), api::enums::BankNames::UlsterBank => Ok(Self::UlsterBank), - _ => Err(errors::ConnectorError::NotSupported { + enums::BankNames::AmericanExpress + | enums::BankNames::AffinBank + | enums::BankNames::AgroBank + | enums::BankNames::AllianceBank + | enums::BankNames::AmBank + | enums::BankNames::BankOfAmerica + | enums::BankNames::BankIslam + | enums::BankNames::BankMuamalat + | enums::BankNames::BankRakyat + | enums::BankNames::BankSimpananNasional + | enums::BankNames::BlikPSP + | enums::BankNames::CapitalOne + | enums::BankNames::Chase + | enums::BankNames::Citi + | enums::BankNames::CimbBank + | enums::BankNames::Discover + | enums::BankNames::NavyFederalCreditUnion + | enums::BankNames::PentagonFederalCreditUnion + | enums::BankNames::SynchronyBank + | enums::BankNames::WellsFargo + | enums::BankNames::AbnAmro + | enums::BankNames::AsnBank + | enums::BankNames::Bunq + | enums::BankNames::Handelsbanken + | enums::BankNames::HongLeongBank + | enums::BankNames::Ing + | enums::BankNames::Knab + | enums::BankNames::KuwaitFinanceHouse + | enums::BankNames::Moneyou + | enums::BankNames::Rabobank + | enums::BankNames::Regiobank + | enums::BankNames::SnsBank + | enums::BankNames::TriodosBank + | enums::BankNames::VanLanschot + | enums::BankNames::ArzteUndApothekerBank + | enums::BankNames::AustrianAnadiBankAg + | enums::BankNames::BankAustria + | enums::BankNames::Bank99Ag + | enums::BankNames::BankhausCarlSpangler + | enums::BankNames::BankhausSchelhammerUndSchatteraAg + | enums::BankNames::BankMillennium + | enums::BankNames::BankPEKAOSA + | enums::BankNames::BawagPskAg + | enums::BankNames::BksBankAg + | enums::BankNames::BrullKallmusBankAg + | enums::BankNames::BtvVierLanderBank + | enums::BankNames::CapitalBankGraweGruppeAg + | enums::BankNames::CeskaSporitelna + | enums::BankNames::Dolomitenbank + | enums::BankNames::EasybankAg + | enums::BankNames::EPlatbyVUB + | enums::BankNames::ErsteBankUndSparkassen + | enums::BankNames::FrieslandBank + | enums::BankNames::HypoAlpeadriabankInternationalAg + | enums::BankNames::HypoNoeLbFurNiederosterreichUWien + | enums::BankNames::HypoOberosterreichSalzburgSteiermark + | enums::BankNames::HypoTirolBankAg + | enums::BankNames::HypoVorarlbergBankAg + | enums::BankNames::HypoBankBurgenlandAktiengesellschaft + | enums::BankNames::KomercniBanka + | enums::BankNames::MBank + | enums::BankNames::MarchfelderBank + | enums::BankNames::Maybank + | enums::BankNames::OberbankAg + | enums::BankNames::OsterreichischeArzteUndApothekerbank + | enums::BankNames::OcbcBank + | enums::BankNames::PayWithING + | enums::BankNames::PlaceZIPKO + | enums::BankNames::PlatnoscOnlineKartaPlatnicza + | enums::BankNames::PosojilnicaBankEGen + | enums::BankNames::PostovaBanka + | enums::BankNames::PublicBank + | enums::BankNames::RaiffeisenBankengruppeOsterreich + | enums::BankNames::RhbBank + | enums::BankNames::SchelhammerCapitalBankAg + | enums::BankNames::StandardCharteredBank + | enums::BankNames::SchoellerbankAg + | enums::BankNames::SpardaBankWien + | enums::BankNames::SporoPay + | enums::BankNames::TatraPay + | enums::BankNames::Viamo + | enums::BankNames::VolksbankGruppe + | enums::BankNames::VolkskreditbankAg + | enums::BankNames::VrBankBraunau + | enums::BankNames::UobBank + | enums::BankNames::PayWithAliorBank + | enums::BankNames::BankiSpoldzielcze + | enums::BankNames::PayWithInteligo + | enums::BankNames::BNPParibasPoland + | enums::BankNames::BankNowySA + | enums::BankNames::CreditAgricole + | enums::BankNames::PayWithBOS + | enums::BankNames::PayWithCitiHandlowy + | enums::BankNames::PayWithPlusBank + | enums::BankNames::ToyotaBank + | enums::BankNames::VeloBank + | enums::BankNames::ETransferPocztowy24 + | enums::BankNames::PlusBank + | enums::BankNames::EtransferPocztowy24 + | enums::BankNames::BankiSpbdzielcze + | enums::BankNames::BankNowyBfgSa + | enums::BankNames::GetinBank + | enums::BankNames::Blik + | enums::BankNames::NoblePay + | enums::BankNames::IdeaBank + | enums::BankNames::EnveloBank + | enums::BankNames::NestPrzelew + | enums::BankNames::MbankMtransfer + | enums::BankNames::Inteligo + | enums::BankNames::PbacZIpko + | enums::BankNames::BnpParibas + | enums::BankNames::BankPekaoSa + | enums::BankNames::VolkswagenBank + | enums::BankNames::AliorBank + | enums::BankNames::Boz + | enums::BankNames::BangkokBank + | enums::BankNames::KrungsriBank + | enums::BankNames::KrungThaiBank + | enums::BankNames::TheSiamCommercialBank + | enums::BankNames::KasikornBank => Err(errors::ConnectorError::NotSupported { message: String::from("BankRedirect"), connector: "Adyen", })?, @@ -2102,7 +2221,12 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod ), api_models::payments::BankRedirectData::OpenBankingUk { issuer, .. } => Ok( AdyenPaymentMethod::OpenBankingUK(Box::new(OpenBankingUKData { - issuer: OpenBankingUKIssuer::try_from(issuer)?, + issuer: match issuer { + Some(bank_name) => OpenBankingUKIssuer::try_from(bank_name)?, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "issuer", + })?, + }, })), ), api_models::payments::BankRedirectData::Sofort { .. } => Ok(AdyenPaymentMethod::Sofort), @@ -2580,7 +2704,7 @@ impl<'a> let additional_data = get_additional_data(item.router_data); let return_url = item.router_data.request.get_return_url()?; let payment_method = AdyenPaymentMethod::try_from(bank_redirect_data)?; - let (shopper_locale, country) = get_redirect_extra_details(item.router_data); + let (shopper_locale, country) = get_redirect_extra_details(item.router_data)?; let line_items = Some(get_line_items(item)); Ok(AdyenPaymentRequest { @@ -2611,7 +2735,7 @@ impl<'a> fn get_redirect_extra_details( item: &types::PaymentsAuthorizeRouterData, -) -> (Option, Option) { +) -> Result<(Option, Option), errors::ConnectorError> { match item.request.payment_method_data { api_models::payments::PaymentMethodData::BankRedirect(ref redirect_data) => { match redirect_data { @@ -2619,17 +2743,20 @@ fn get_redirect_extra_details( country, preferred_language, .. - } => ( + } => Ok(( Some(preferred_language.to_string()), Some(country.to_owned()), - ), + )), api_models::payments::BankRedirectData::OpenBankingUk { country, .. } => { - (None, Some(country.to_owned())) + let country = country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "country", + })?; + Ok((None, Some(country))) } - _ => (None, None), + _ => Ok((None, None)), } } - _ => (None, None), + _ => Ok((None, None)), } } From 44b1f4949ea06d59480670ccfa02446fa7713d13 Mon Sep 17 00:00:00 2001 From: Sudheer konagalla <50401745+cb-sudheer@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:57:38 +0530 Subject: [PATCH 109/443] fix(router): [Dlocal] connector transaction id fix (#2872) --- .../src/connector/dlocal/transformers.rs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index a9033e53d666..f7cfa6a868bd 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -303,7 +303,7 @@ pub struct DlocalPaymentsResponse { status: DlocalPaymentStatus, id: String, three_dsecure: Option, - order_id: String, + order_id: Option, } impl @@ -323,12 +323,12 @@ impl }); let response = types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.order_id.clone()), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), }; Ok(Self { status: enums::AttemptStatus::from(item.response.status), @@ -342,7 +342,7 @@ impl pub struct DlocalPaymentsSyncResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -362,14 +362,12 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.order_id.clone(), - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), }), ..item.data }) @@ -380,7 +378,7 @@ impl pub struct DlocalPaymentsCaptureResponse { status: DlocalPaymentStatus, id: String, - order_id: String, + order_id: Option, } impl @@ -400,14 +398,12 @@ impl Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.order_id.clone(), - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: Some(item.response.order_id.clone()), + connector_response_reference_id: item.response.order_id.clone(), }), ..item.data }) From 39f255b4b209588dec35d780078c2ab7ceb37b10 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:06:35 +0530 Subject: [PATCH 110/443] feat(core): Add ability to verify connector credentials before integrating the connector (#2986) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/admin.rs | 32 ++++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/verify_connector.rs | 11 ++ crates/router/src/consts.rs | 5 + crates/router/src/core.rs | 2 + crates/router/src/core/verify_connector.rs | 63 ++++++ crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 6 + crates/router/src/routes/lock_utils.rs | 4 +- crates/router/src/routes/verify_connector.rs | 28 +++ crates/router/src/types.rs | 74 ++++++- crates/router/src/types/api.rs | 2 + .../router/src/types/api/verify_connector.rs | 181 ++++++++++++++++++ .../src/types/api/verify_connector/paypal.rs | 54 ++++++ .../src/types/api/verify_connector/stripe.rs | 36 ++++ crates/router/src/utils.rs | 2 + crates/router/src/utils/verify_connector.rs | 49 +++++ crates/router_env/src/logger/types.rs | 2 + 18 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 crates/api_models/src/verify_connector.rs create mode 100644 crates/router/src/core/verify_connector.rs create mode 100644 crates/router/src/routes/verify_connector.rs create mode 100644 crates/router/src/types/api/verify_connector.rs create mode 100644 crates/router/src/types/api/verify_connector/paypal.rs create mode 100644 crates/router/src/types/api/verify_connector/stripe.rs create mode 100644 crates/router/src/utils/verify_connector.rs diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index efde4a048323..6bb4fd4afa0f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use common_utils::{ crypto::{Encryptable, OptionalEncryptableName}, pii, @@ -614,6 +616,36 @@ pub struct MerchantConnectorCreate { pub status: Option, } +// Different patterns of authentication. +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(tag = "auth_type")] +pub enum ConnectorAuthType { + TemporaryAuth, + HeaderKey { + api_key: Secret, + }, + BodyKey { + api_key: Secret, + key1: Secret, + }, + SignatureKey { + api_key: Secret, + key1: Secret, + api_secret: Secret, + }, + MultiAuthKey { + api_key: Secret, + key1: Secret, + api_secret: Secret, + key2: Secret, + }, + CurrencyAuthKey { + auth_key_map: HashMap, + }, + #[default] + NoKey, +} + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(deny_unknown_fields)] pub struct MerchantConnectorWebhookDetails { diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index ab40a96582bb..8ef40d319140 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -27,4 +27,5 @@ pub mod routing; pub mod surcharge_decision_configs; pub mod user; pub mod verifications; +pub mod verify_connector; pub mod webhooks; diff --git a/crates/api_models/src/verify_connector.rs b/crates/api_models/src/verify_connector.rs new file mode 100644 index 000000000000..1db5a19a030a --- /dev/null +++ b/crates/api_models/src/verify_connector.rs @@ -0,0 +1,11 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::{admin, enums}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct VerifyConnectorRequest { + pub connector_name: enums::Connector, + pub connector_account_details: admin::ConnectorAuthType, +} + +common_utils::impl_misc_api_event_type!(VerifyConnectorRequest); diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 61072d06221b..49c5cfacad1f 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -65,3 +65,8 @@ pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days #[cfg(feature = "email")] pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; + +#[cfg(feature = "olap")] +pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; +#[cfg(feature = "olap")] +pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index cff2dc8e58f1..30fe1a1ce8cb 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -28,4 +28,6 @@ pub mod user; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; diff --git a/crates/router/src/core/verify_connector.rs b/crates/router/src/core/verify_connector.rs new file mode 100644 index 000000000000..e837e8b8b259 --- /dev/null +++ b/crates/router/src/core/verify_connector.rs @@ -0,0 +1,63 @@ +use api_models::{enums::Connector, verify_connector::VerifyConnectorRequest}; +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + connector, + core::errors, + services, + types::{ + api, + api::verify_connector::{self as types, VerifyConnector}, + }, + utils::verify_connector as utils, + AppState, +}; + +pub async fn verify_connector_credentials( + state: AppState, + req: VerifyConnectorRequest, +) -> errors::RouterResponse<()> { + let boxed_connector = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &req.connector_name.to_string(), + api::GetToken::Connector, + None, + ) + .change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven)?; + + let card_details = utils::get_test_card_details(req.connector_name)? + .ok_or(errors::ApiErrorResponse::FlowNotSupported { + flow: "Verify credentials".to_string(), + connector: req.connector_name.to_string(), + }) + .into_report()?; + + match req.connector_name { + Connector::Stripe => { + connector::Stripe::verify( + &state, + types::VerifyConnectorData { + connector: *boxed_connector.connector, + connector_auth: req.connector_account_details.into(), + card_details, + }, + ) + .await + } + Connector::Paypal => connector::Paypal::get_access_token( + &state, + types::VerifyConnectorData { + connector: *boxed_connector.connector, + connector_auth: req.connector_account_details.into(), + card_details, + }, + ) + .await + .map(|_| services::ApplicationResponse::StatusOk), + _ => Err(errors::ApiErrorResponse::FlowNotSupported { + flow: "Verify credentials".to_string(), + connector: req.connector_name.to_string(), + }) + .into_report(), + } +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 37cc1339e1a1..22c2610d3255 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -29,6 +29,8 @@ pub mod routing; pub mod user; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; pub mod locker_migration; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 80993429c4e2..2a7e1ab61905 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -30,6 +30,8 @@ use super::{cache::*, health::*}; use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; +#[cfg(feature = "olap")] +use crate::routes::verify_connector::payment_connector_verify; pub use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, @@ -548,6 +550,10 @@ impl MerchantConnectorAccount { use super::admin::*; route = route + .service( + web::resource("/connectors/verify") + .route(web::post().to(payment_connector_verify)), + ) .service( web::resource("/{merchant_id}/connectors") .route(web::post().to(payment_connector_create)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 5c2ad123749c..c7369b9e4d52 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -147,7 +147,9 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount | Flow::ChangePassword => Self::User, + Flow::UserConnectAccount | Flow::ChangePassword | Flow::VerifyPaymentConnector => { + Self::User + } } } } diff --git a/crates/router/src/routes/verify_connector.rs b/crates/router/src/routes/verify_connector.rs new file mode 100644 index 000000000000..bfb1b781ada4 --- /dev/null +++ b/crates/router/src/routes/verify_connector.rs @@ -0,0 +1,28 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::verify_connector::VerifyConnectorRequest; +use router_env::{instrument, tracing, Flow}; + +use super::AppState; +use crate::{ + core::{api_locking, verify_connector}, + services::{self, authentication as auth, authorization::permissions::Permission}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::VerifyPaymentConnector))] +pub async fn payment_connector_verify( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyPaymentConnector; + Box::pin(services::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _: (), req| verify_connector::verify_connector_credentials(state, req), + &auth::JWTAuth(Permission::MerchantConnectorAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index cd37fbb549d9..c3118f0c05be 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -33,7 +33,7 @@ use crate::{ payments::{PaymentData, RecurringMandatePaymentData}, }, services, - types::storage::payment_attempt::PaymentAttemptExt, + types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, utils::OptionExt, }; @@ -942,6 +942,78 @@ pub enum ConnectorAuthType { NoKey, } +impl From for ConnectorAuthType { + fn from(value: api_models::admin::ConnectorAuthType) -> Self { + match value { + api_models::admin::ConnectorAuthType::TemporaryAuth => Self::TemporaryAuth, + api_models::admin::ConnectorAuthType::HeaderKey { api_key } => { + Self::HeaderKey { api_key } + } + api_models::admin::ConnectorAuthType::BodyKey { api_key, key1 } => { + Self::BodyKey { api_key, key1 } + } + api_models::admin::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Self::SignatureKey { + api_key, + key1, + api_secret, + }, + api_models::admin::ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Self::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + }, + api_models::admin::ConnectorAuthType::CurrencyAuthKey { auth_key_map } => { + Self::CurrencyAuthKey { auth_key_map } + } + api_models::admin::ConnectorAuthType::NoKey => Self::NoKey, + } + } +} + +impl ForeignFrom for api_models::admin::ConnectorAuthType { + fn foreign_from(from: ConnectorAuthType) -> Self { + match from { + ConnectorAuthType::TemporaryAuth => Self::TemporaryAuth, + ConnectorAuthType::HeaderKey { api_key } => Self::HeaderKey { api_key }, + ConnectorAuthType::BodyKey { api_key, key1 } => Self::BodyKey { api_key, key1 }, + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Self::SignatureKey { + api_key, + key1, + api_secret, + }, + ConnectorAuthType::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + } => Self::MultiAuthKey { + api_key, + key1, + api_secret, + key2, + }, + ConnectorAuthType::CurrencyAuthKey { auth_key_map } => { + Self::CurrencyAuthKey { auth_key_map } + } + ConnectorAuthType::NoKey => Self::NoKey, + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConnectorsList { pub connectors: Vec, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index bcb3a9add553..96bcaca3ed5d 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -13,6 +13,8 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +#[cfg(feature = "olap")] +pub mod verify_connector; pub mod webhooks; use std::{fmt::Debug, str::FromStr}; diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs new file mode 100644 index 000000000000..3e3511ccb98f --- /dev/null +++ b/crates/router/src/types/api/verify_connector.rs @@ -0,0 +1,181 @@ +pub mod paypal; +pub mod stripe; + +use error_stack::{IntoReport, ResultExt}; + +use crate::{ + consts, + core::errors, + services, + services::ConnectorIntegration, + types::{self, api, storage::enums as storage_enums}, + AppState, +}; + +#[derive(Clone, Debug)] +pub struct VerifyConnectorData { + pub connector: &'static (dyn types::api::Connector + Sync), + pub connector_auth: types::ConnectorAuthType, + pub card_details: api::Card, +} + +impl VerifyConnectorData { + fn get_payment_authorize_data(&self) -> types::PaymentsAuthorizeData { + types::PaymentsAuthorizeData { + payment_method_data: api::PaymentMethodData::Card(self.card_details.clone()), + email: None, + amount: 1000, + confirm: true, + currency: storage_enums::Currency::USD, + mandate_id: None, + webhook_url: None, + customer_id: None, + off_session: None, + browser_info: None, + session_token: None, + order_details: None, + order_category: None, + capture_method: None, + enrolled_for_3ds: false, + router_return_url: None, + surcharge_details: None, + setup_future_usage: None, + payment_experience: None, + payment_method_type: None, + statement_descriptor: None, + setup_mandate_details: None, + complete_authorize_url: None, + related_transaction_id: None, + statement_descriptor_suffix: None, + } + } + + fn get_router_data( + &self, + request_data: R1, + access_token: Option, + ) -> types::RouterData { + let attempt_id = + common_utils::generate_id_with_default_len(consts::VERIFY_CONNECTOR_ID_PREFIX); + types::RouterData { + flow: std::marker::PhantomData, + status: storage_enums::AttemptStatus::Started, + request: request_data, + response: Err(errors::ApiErrorResponse::InternalServerError.into()), + connector: self.connector.id().to_string(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + test_mode: None, + return_url: None, + attempt_id: attempt_id.clone(), + description: None, + customer_id: None, + merchant_id: consts::VERIFY_CONNECTOR_MERCHANT_ID.to_string(), + reference_id: None, + access_token, + session_token: None, + payment_method: storage_enums::PaymentMethod::Card, + amount_captured: None, + preprocessing_id: None, + payment_method_id: None, + connector_customer: None, + connector_auth_type: self.connector_auth.clone(), + connector_meta_data: None, + payment_method_token: None, + connector_api_version: None, + recurring_mandate_payment_data: None, + connector_request_reference_id: attempt_id, + address: types::PaymentAddress { + shipping: None, + billing: None, + }, + payment_id: common_utils::generate_id_with_default_len( + consts::VERIFY_CONNECTOR_ID_PREFIX, + ), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + } + } +} + +#[async_trait::async_trait] +pub trait VerifyConnector { + async fn verify( + state: &AppState, + connector_data: VerifyConnectorData, + ) -> errors::RouterResponse<()> { + let authorize_data = connector_data.get_payment_authorize_data(); + let access_token = Self::get_access_token(state, connector_data.clone()).await?; + let router_data = connector_data.get_router_data(authorize_data, access_token); + + let request = connector_data + .connector + .build_request(&router_data, &state.conf.connectors) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment request cannot be built".to_string(), + })? + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + + let response = services::call_connector_api(&state.to_owned(), request) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response { + Ok(_) => Ok(services::ApplicationResponse::StatusOk), + Err(error_response) => { + Self::handle_payment_error_response::< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >(connector_data.connector, error_response) + .await + } + } + } + + async fn get_access_token( + _state: &AppState, + _connector_data: VerifyConnectorData, + ) -> errors::CustomResult, errors::ApiErrorResponse> { + // AccessToken is None for the connectors without the AccessToken Flow. + // If a connector has that, then it should override this implementation. + Ok(None) + } + + async fn handle_payment_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResponse<()> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report() + } + + async fn handle_access_token_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResult> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report() + } +} diff --git a/crates/router/src/types/api/verify_connector/paypal.rs b/crates/router/src/types/api/verify_connector/paypal.rs new file mode 100644 index 000000000000..33e848f909df --- /dev/null +++ b/crates/router/src/types/api/verify_connector/paypal.rs @@ -0,0 +1,54 @@ +use error_stack::ResultExt; + +use super::{VerifyConnector, VerifyConnectorData}; +use crate::{ + connector, + core::errors, + routes::AppState, + services, + types::{self, api}, +}; + +#[async_trait::async_trait] +impl VerifyConnector for connector::Paypal { + async fn get_access_token( + state: &AppState, + connector_data: VerifyConnectorData, + ) -> errors::CustomResult, errors::ApiErrorResponse> { + let token_data: types::AccessTokenRequestData = + connector_data.connector_auth.clone().try_into()?; + let router_data = connector_data.get_router_data(token_data, None); + + let request = connector_data + .connector + .build_request(&router_data, &state.conf.connectors) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment request cannot be built".to_string(), + })? + .ok_or(errors::ApiErrorResponse::InternalServerError)?; + + let response = services::call_connector_api(&state.to_owned(), request) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + match response { + Ok(res) => Some( + connector_data + .connector + .handle_response(&router_data, res) + .change_context(errors::ApiErrorResponse::InternalServerError)? + .response + .map_err(|_| errors::ApiErrorResponse::InternalServerError.into()), + ) + .transpose(), + Err(response_data) => { + Self::handle_access_token_error_response::< + api::AccessTokenAuth, + types::AccessTokenRequestData, + types::AccessToken, + >(connector_data.connector, response_data) + .await + } + } + } +} diff --git a/crates/router/src/types/api/verify_connector/stripe.rs b/crates/router/src/types/api/verify_connector/stripe.rs new file mode 100644 index 000000000000..ece9fa15a1d9 --- /dev/null +++ b/crates/router/src/types/api/verify_connector/stripe.rs @@ -0,0 +1,36 @@ +use error_stack::{IntoReport, ResultExt}; +use router_env::env; + +use super::VerifyConnector; +use crate::{ + connector, + core::errors, + services::{self, ConnectorIntegration}, + types, +}; + +#[async_trait::async_trait] +impl VerifyConnector for connector::Stripe { + async fn handle_payment_error_response( + connector: &(dyn types::api::Connector + Sync), + error_response: types::Response, + ) -> errors::RouterResponse<()> + where + dyn types::api::Connector + Sync: ConnectorIntegration, + { + let error = connector + .get_error_response(error_response) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + match (env::which(), error.code.as_str()) { + // In situations where an attempt is made to process a payment using a + // Stripe production key along with a test card (which verify_connector is using), + // Stripe will respond with a "card_declined" error. In production, + // when this scenario occurs we will send back an "Ok" response. + (env::Env::Production, "card_declined") => Ok(services::ApplicationResponse::StatusOk), + _ => Err(errors::ApiErrorResponse::InvalidRequestData { + message: error.reason.unwrap_or(error.message), + }) + .into_report(), + } + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index c936ee858c17..81968cd9b628 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -6,6 +6,8 @@ pub mod ext_traits; pub mod storage_partitioning; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod verify_connector; use std::fmt::Debug; diff --git a/crates/router/src/utils/verify_connector.rs b/crates/router/src/utils/verify_connector.rs new file mode 100644 index 000000000000..6ad683d63ba1 --- /dev/null +++ b/crates/router/src/utils/verify_connector.rs @@ -0,0 +1,49 @@ +use api_models::enums::Connector; +use error_stack::{IntoReport, ResultExt}; + +use crate::{core::errors, types::api}; + +pub fn generate_card_from_details( + card_number: String, + card_exp_year: String, + card_exp_month: String, + card_cvv: String, +) -> errors::RouterResult { + Ok(api::Card { + card_number: card_number + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing card number")?, + card_issuer: None, + card_cvc: masking::Secret::new(card_cvv), + card_network: None, + card_exp_year: masking::Secret::new(card_exp_year), + card_exp_month: masking::Secret::new(card_exp_month), + card_holder_name: masking::Secret::new("HyperSwitch".to_string()), + nick_name: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + }) +} + +pub fn get_test_card_details(connector_name: Connector) -> errors::RouterResult> { + match connector_name { + Connector::Stripe => Some(generate_card_from_details( + "4242424242424242".to_string(), + "2025".to_string(), + "12".to_string(), + "100".to_string(), + )) + .transpose(), + Connector::Paypal => Some(generate_card_from_details( + "4111111111111111".to_string(), + "2025".to_string(), + "02".to_string(), + "123".to_string(), + )) + .transpose(), + _ => Ok(None), + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 2a174f42eb63..c254f89b4eef 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -259,6 +259,8 @@ pub enum Flow { DecisionManagerRetrieveConfig, /// Change password flow ChangePassword, + /// Payment Connector Verify + VerifyPaymentConnector, } /// From 663754d629d59a17ba9d4985fe04f9404ceb16b7 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:59:35 +0530 Subject: [PATCH 111/443] fix(connector): move authorised status to charged in setup mandate (#3017) --- crates/router/src/connector/cybersource.rs | 17 ++--- .../src/connector/cybersource/transformers.rs | 74 ++++++++++++++++++- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 1868611184f9..1de107af086d 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -307,18 +307,15 @@ impl data: &types::SetupMandateRouterData, res: types::Response, ) -> CustomResult { - let response: cybersource::CybersourcePaymentsResponse = res + let response: cybersource::CybersourceSetupMandatesResponse = res .response - .parse_struct("CybersourceMandateResponse") + .parse_struct("CybersourceSetupMandatesResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(( - types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }, - false, - )) + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) } fn get_error_response( diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 656c45b6d6b6..81df29966725 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -499,6 +499,16 @@ pub struct CybersourcePaymentsResponse { token_information: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceSetupMandatesResponse { + id: String, + status: CybersourcePaymentStatus, + error_information: Option, + client_reference_information: Option, + token_information: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { @@ -553,7 +563,69 @@ impl reason: Some(error.reason), status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(item.response.id), + }), + _ => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.id.clone(), + ), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: item + .response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(item.response.id)), + }), + }, + ..item.data + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourceSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourceSetupMandatesResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let mandate_reference = + item.response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); + let mut mandate_status: enums::AttemptStatus = item.response.status.into(); + if matches!(mandate_status, enums::AttemptStatus::Authorized) { + //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. + mandate_status = enums::AttemptStatus::Charged + } + Ok(Self { + status: mandate_status, + response: match item.response.error_information { + Some(error) => Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error.message, + reason: Some(error.reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.id), }), _ => Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( From b1fe76a82b4026d6eaa3baf4356378040880a458 Mon Sep 17 00:00:00 2001 From: Shanks Date: Thu, 30 Nov 2023 14:47:27 +0530 Subject: [PATCH 112/443] fix(router): use default value for the routing algorithm column during business profile creation (#2791) --- crates/router/src/types/api/admin.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 6bbe9149f4d7..fe99d084223a 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -124,9 +124,10 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), webhook_details: webhook_details.or(merchant_account.webhook_details), metadata: request.metadata, - routing_algorithm: request - .routing_algorithm - .or(merchant_account.routing_algorithm), + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), intent_fulfillment_time: request .intent_fulfillment_time .map(i64::from) From 6a2e4ab4169820f35e953a949bd2e82e7f098ed2 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:58:37 +0530 Subject: [PATCH 113/443] feat(user): add support for dashboard metadata (#3000) Co-authored-by: Rachit Naithani <81706961+racnan@users.noreply.github.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Co-authored-by: Arjun Karthik Co-authored-by: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 15 +- crates/api_models/src/user.rs | 1 + .../api_models/src/user/dashboard_metadata.rs | 110 ++++ crates/diesel_models/src/enums.rs | 36 ++ crates/diesel_models/src/query.rs | 1 + .../src/query/dashboard_metadata.rs | 64 +++ crates/diesel_models/src/schema.rs | 25 + crates/diesel_models/src/user.rs | 2 + .../src/user/dashboard_metadata.rs | 35 ++ crates/router/src/core/errors/user.rs | 26 +- crates/router/src/core/user.rs | 2 + .../src/core/user/dashboard_metadata.rs | 537 ++++++++++++++++++ crates/router/src/db.rs | 2 + crates/router/src/db/dashboard_metadata.rs | 184 ++++++ crates/router/src/db/kafka_store.rs | 38 +- crates/router/src/routes/app.rs | 5 + crates/router/src/routes/lock_utils.rs | 8 +- crates/router/src/routes/user.rs | 57 +- crates/router/src/types/domain/user.rs | 2 + .../types/domain/user/dashboard_metadata.rs | 56 ++ crates/router/src/types/storage.rs | 11 +- .../src/types/storage/dashboard_metadata.rs | 1 + crates/router/src/utils/user.rs | 1 + .../src/utils/user/dashboard_metadata.rs | 162 ++++++ crates/router_env/src/logger/types.rs | 4 + crates/storage_impl/src/mock_db.rs | 2 + .../down.sql | 3 + .../up.sql | 15 + 28 files changed, 1389 insertions(+), 16 deletions(-) create mode 100644 crates/api_models/src/user/dashboard_metadata.rs create mode 100644 crates/diesel_models/src/query/dashboard_metadata.rs create mode 100644 crates/diesel_models/src/user/dashboard_metadata.rs create mode 100644 crates/router/src/core/user/dashboard_metadata.rs create mode 100644 crates/router/src/db/dashboard_metadata.rs create mode 100644 crates/router/src/types/domain/user/dashboard_metadata.rs create mode 100644 crates/router/src/types/storage/dashboard_metadata.rs create mode 100644 crates/router/src/utils/user/dashboard_metadata.rs create mode 100644 migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql create mode 100644 migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 4e9f2f284173..edfdcf1d6652 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,6 +1,11 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse}; +use crate::user::{ + dashboard_metadata::{ + GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, + }, + ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, +}; impl ApiEventMetric for ConnectAccountResponse { fn get_api_event_type(&self) -> Option { @@ -13,4 +18,10 @@ impl ApiEventMetric for ConnectAccountResponse { impl ApiEventMetric for ConnectAccountRequest {} -common_utils::impl_misc_api_event_type!(ChangePasswordRequest); +common_utils::impl_misc_api_event_type!( + ChangePasswordRequest, + GetMultipleMetaDataPayload, + GetMetaDataResponse, + GetMetaDataRequest, + SetMetaDataRequest +); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 41ea9cc5193a..84659432aa6a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,5 +1,6 @@ use common_utils::pii; use masking::Secret; +pub mod dashboard_metadata; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] pub struct ConnectAccountRequest { diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..04cda3bd7075 --- /dev/null +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -0,0 +1,110 @@ +use masking::Secret; +use strum::EnumString; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub enum SetMetaDataRequest { + ProductionAgreement(ProductionAgreementRequest), + SetupProcessor(SetupProcessor), + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected(ProcessorConnected), + SecondProcessorConnected(ProcessorConnected), + ConfiguredRouting(ConfiguredRouting), + TestPayment(TestPayment), + IntegrationMethod(IntegrationMethod), + IntegrationCompleted, + SPRoutingConfigured(ConfiguredRouting), + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProductionAgreementRequest { + pub version: String, + #[serde(skip_deserializing)] + pub ip_address: Option>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SetupProcessor { + pub connector_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProcessorConnected { + pub processor_id: String, + pub processor_name: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ConfiguredRouting { + pub routing_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestPayment { + pub payment_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct IntegrationMethod { + pub integration_type: String, +} + +#[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] +pub enum GetMetaDataRequest { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SPRoutingConfigured, + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(transparent)] +pub struct GetMultipleMetaDataPayload { + pub results: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetMultipleMetaDataRequest { + pub keys: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum GetMetaDataResponse { + ProductionAgreement(bool), + SetupProcessor(Option), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(Option), + SecondProcessorConnected(Option), + ConfiguredRouting(Option), + TestPayment(Option), + IntegrationMethod(Option), + IntegrationCompleted(bool), + StripeConnected(Option), + PaypalConnected(Option), + SPRoutingConfigured(Option), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index dc4a7614f587..3ddd85f37891 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -425,3 +425,39 @@ pub enum UserStatus { #[default] InvitationSent, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DashboardMetadata { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SpRoutingConfigured, + SpTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index cf5a993c2686..b0537d0a287b 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -6,6 +6,7 @@ pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod events; pub mod file; diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs new file mode 100644 index 000000000000..03e4a2dab38b --- /dev/null +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -0,0 +1,64 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{ + enums, + query::generics, + schema::dashboard_metadata::dsl, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + PgPooledConn, StorageResult, +}; + +impl DashboardMetadataNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl DashboardMetadata { + pub async fn find_user_scoped_dashboard_metadata( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } + + pub async fn find_merchant_scoped_dashboard_metadata( + conn: &PgPooledConn, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .is_null() + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 33400635f052..6cab6d5730d0 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -183,6 +183,30 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + dashboard_metadata (id) { + id -> Int4, + #[max_length = 64] + user_id -> Nullable, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + org_id -> Varchar, + #[max_length = 64] + data_key -> Varchar, + data_value -> Json, + #[max_length = 64] + created_by -> Varchar, + created_at -> Timestamp, + #[max_length = 64] + last_modified_by -> Varchar, + last_modified_at -> Timestamp, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -965,6 +989,7 @@ diesel::allow_tables_to_appear_in_same_query!( cards_info, configs, customers, + dashboard_metadata, dispute, events, file_metadata, diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 6a2e864b291c..4eec710ea185 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -5,6 +5,8 @@ use time::PrimitiveDateTime; use crate::schema::users; +pub mod dashboard_metadata; + #[derive(Clone, Debug, Identifiable, Queryable)] #[diesel(table_name = users)] pub struct User { diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..018808f1c0db --- /dev/null +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -0,0 +1,35 @@ +use diesel::{query_builder::AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::{enums, schema::dashboard_metadata}; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadata { + pub id: i32, + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataNew { + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index b86c395b9814..f5c50e28ccc6 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -27,10 +27,16 @@ pub enum UserErrors { MerchantAccountCreationError(String), #[error("InvalidEmailError")] InvalidEmailError, - #[error("DuplicateOrganizationId")] - DuplicateOrganizationId, #[error("MerchantIdNotFound")] MerchantIdNotFound, + #[error("MetadataAlreadySet")] + MetadataAlreadySet, + #[error("DuplicateOrganizationId")] + DuplicateOrganizationId, + #[error("IpAddressParsingFailed")] + IpAddressParsingFailed, + #[error("InvalidMetadataRequest")] + InvalidMetadataRequest, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -77,15 +83,27 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) } + Self::MerchantIdNotFound => { + AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + } + Self::MetadataAlreadySet => { + AER::BadRequest(ApiError::new(sub_code, 19, "Metadata already set", None)) + } Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( sub_code, 21, "An Organization with the id already exists", None, )), - Self::MerchantIdNotFound => { - AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + Self::IpAddressParsingFailed => { + AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) } + Self::InvalidMetadataRequest => AER::BadRequest(ApiError::new( + sub_code, + 26, + "Invalid Metadata Request", + None, + )), } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 1dc0e2e1a112..9a199d09b8fd 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -13,6 +13,8 @@ use crate::{ types::domain, }; +pub mod dashboard_metadata; + pub async fn connect_account( state: AppState, request: api::ConnectAccountRequest, diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs new file mode 100644 index 000000000000..de385fb8ed65 --- /dev/null +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -0,0 +1,537 @@ +use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, +}; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse, UserResult}, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + types::domain::{user::dashboard_metadata as types, MerchantKeyStore}, + utils::user::dashboard_metadata as utils, +}; + +pub async fn set_metadata( + state: AppState, + user: UserFromToken, + request: api::SetMetaDataRequest, +) -> UserResponse<()> { + let metadata_value = parse_set_request(request)?; + let metadata_key = DBEnum::from(&metadata_value); + + insert_metadata(&state, user, metadata_key, metadata_value).await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn get_multiple_metadata( + state: AppState, + user: UserFromToken, + request: GetMultipleMetaDataPayload, +) -> UserResponse> { + let metadata_keys: Vec = request.results.into_iter().map(parse_get_request).collect(); + + let metadata = fetch_metadata(&state, &user, metadata_keys.clone()).await?; + + let mut response = Vec::with_capacity(metadata_keys.len()); + for key in metadata_keys { + let data = metadata.iter().find(|ele| ele.data_key == key); + let resp; + if data.is_none() && utils::is_backfill_required(&key) { + let backfill_data = backfill_metadata(&state, &user, &key).await?; + resp = into_response(backfill_data.as_ref(), &key)?; + } else { + resp = into_response(data, &key)?; + } + response.push(resp); + } + + Ok(ApplicationResponse::Json(response)) +} + +fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult { + match data_enum { + api::SetMetaDataRequest::ProductionAgreement(req) => { + let ip_address = req + .ip_address + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Error Getting Ip Address")?; + Ok(types::MetaData::ProductionAgreement( + types::ProductionAgreementValue { + version: req.version, + ip_address, + timestamp: common_utils::date_time::now(), + }, + )) + } + api::SetMetaDataRequest::SetupProcessor(req) => Ok(types::MetaData::SetupProcessor(req)), + api::SetMetaDataRequest::ConfigureEndpoint => Ok(types::MetaData::ConfigureEndpoint(true)), + api::SetMetaDataRequest::SetupComplete => Ok(types::MetaData::SetupComplete(true)), + api::SetMetaDataRequest::FirstProcessorConnected(req) => { + Ok(types::MetaData::FirstProcessorConnected(req)) + } + api::SetMetaDataRequest::SecondProcessorConnected(req) => { + Ok(types::MetaData::SecondProcessorConnected(req)) + } + api::SetMetaDataRequest::ConfiguredRouting(req) => { + Ok(types::MetaData::ConfiguredRouting(req)) + } + api::SetMetaDataRequest::TestPayment(req) => Ok(types::MetaData::TestPayment(req)), + api::SetMetaDataRequest::IntegrationMethod(req) => { + Ok(types::MetaData::IntegrationMethod(req)) + } + api::SetMetaDataRequest::IntegrationCompleted => { + Ok(types::MetaData::IntegrationCompleted(true)) + } + api::SetMetaDataRequest::SPRoutingConfigured(req) => { + Ok(types::MetaData::SPRoutingConfigured(req)) + } + api::SetMetaDataRequest::SPTestPayment => Ok(types::MetaData::SPTestPayment(true)), + api::SetMetaDataRequest::DownloadWoocom => Ok(types::MetaData::DownloadWoocom(true)), + api::SetMetaDataRequest::ConfigureWoocom => Ok(types::MetaData::ConfigureWoocom(true)), + api::SetMetaDataRequest::SetupWoocomWebhook => { + Ok(types::MetaData::SetupWoocomWebhook(true)) + } + api::SetMetaDataRequest::IsMultipleConfiguration => { + Ok(types::MetaData::IsMultipleConfiguration(true)) + } + } +} + +fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum { + match data_enum { + api::GetMetaDataRequest::ProductionAgreement => DBEnum::ProductionAgreement, + api::GetMetaDataRequest::SetupProcessor => DBEnum::SetupProcessor, + api::GetMetaDataRequest::ConfigureEndpoint => DBEnum::ConfigureEndpoint, + api::GetMetaDataRequest::SetupComplete => DBEnum::SetupComplete, + api::GetMetaDataRequest::FirstProcessorConnected => DBEnum::FirstProcessorConnected, + api::GetMetaDataRequest::SecondProcessorConnected => DBEnum::SecondProcessorConnected, + api::GetMetaDataRequest::ConfiguredRouting => DBEnum::ConfiguredRouting, + api::GetMetaDataRequest::TestPayment => DBEnum::TestPayment, + api::GetMetaDataRequest::IntegrationMethod => DBEnum::IntegrationMethod, + api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, + api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, + api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, + api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::SPTestPayment => DBEnum::SpTestPayment, + api::GetMetaDataRequest::DownloadWoocom => DBEnum::DownloadWoocom, + api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom, + api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook, + api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration, + } +} + +fn into_response( + data: Option<&DashboardMetadata>, + data_type: &DBEnum, +) -> UserResult { + match data_type { + DBEnum::ProductionAgreement => Ok(api::GetMetaDataResponse::ProductionAgreement( + data.is_some(), + )), + DBEnum::SetupProcessor => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SetupProcessor(resp)) + } + DBEnum::ConfigureEndpoint => { + Ok(api::GetMetaDataResponse::ConfigureEndpoint(data.is_some())) + } + DBEnum::SetupComplete => Ok(api::GetMetaDataResponse::SetupComplete(data.is_some())), + DBEnum::FirstProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::FirstProcessorConnected(resp)) + } + DBEnum::SecondProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SecondProcessorConnected(resp)) + } + DBEnum::ConfiguredRouting => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfiguredRouting(resp)) + } + DBEnum::TestPayment => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::TestPayment(resp)) + } + DBEnum::IntegrationMethod => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::IntegrationMethod(resp)) + } + DBEnum::IntegrationCompleted => Ok(api::GetMetaDataResponse::IntegrationCompleted( + data.is_some(), + )), + DBEnum::StripeConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::StripeConnected(resp)) + } + DBEnum::PaypalConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::PaypalConnected(resp)) + } + DBEnum::SpRoutingConfigured => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SPRoutingConfigured(resp)) + } + DBEnum::SpTestPayment => Ok(api::GetMetaDataResponse::SPTestPayment(data.is_some())), + DBEnum::DownloadWoocom => Ok(api::GetMetaDataResponse::DownloadWoocom(data.is_some())), + DBEnum::ConfigureWoocom => Ok(api::GetMetaDataResponse::ConfigureWoocom(data.is_some())), + DBEnum::SetupWoocomWebhook => { + Ok(api::GetMetaDataResponse::SetupWoocomWebhook(data.is_some())) + } + + DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration( + data.is_some(), + )), + } +} + +async fn insert_metadata( + state: &AppState, + user: UserFromToken, + metadata_key: DBEnum, + metadata_value: types::MetaData, +) -> UserResult { + match metadata_value { + types::MetaData::ProductionAgreement(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupProcessor(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureEndpoint(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupComplete(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::FirstProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SecondProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfiguredRouting(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::TestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationMethod(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationCompleted(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::StripeConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::PaypalConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPRoutingConfigured(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPTestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::DownloadWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupWoocomWebhook(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IsMultipleConfiguration(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + } +} + +async fn fetch_metadata( + state: &AppState, + user: &UserFromToken, + metadata_keys: Vec, +) -> UserResult> { + let mut dashboard_metadata = Vec::with_capacity(metadata_keys.len()); + let (merchant_scoped_enums, _) = utils::separate_metadata_type_based_on_scope(metadata_keys); + + if !merchant_scoped_enums.is_empty() { + let mut res = utils::get_merchant_scoped_metadata_from_db( + state, + user.merchant_id.to_owned(), + user.org_id.to_owned(), + merchant_scoped_enums, + ) + .await?; + dashboard_metadata.append(&mut res); + } + + Ok(dashboard_metadata) +} + +pub async fn backfill_metadata( + state: &AppState, + user: &UserFromToken, + key: &DBEnum, +) -> UserResult> { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &user.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + match key { + DBEnum::StripeConnected => { + let mca = if let Some(stripe_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Stripe + .to_string() + .as_str(), + &key_store, + ) + .await? + { + stripe_connected + } else if let Some(stripe_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "stripe_test", + &key_store, + ) + .await? + { + stripe_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::StripeConnected, + types::MetaData::StripeConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + DBEnum::PaypalConnected => { + let mca = if let Some(paypal_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Paypal + .to_string() + .as_str(), + &key_store, + ) + .await? + { + paypal_connected + } else if let Some(paypal_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "paypal_test", + &key_store, + ) + .await? + { + paypal_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::PaypalConnected, + types::MetaData::PaypalConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + _ => Ok(None), + } +} + +pub async fn get_merchant_connector_account_by_name( + state: &AppState, + merchant_id: &str, + connector_name: &str, + key_store: &MerchantKeyStore, +) -> UserResult> { + state + .store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + .map_err(|e| { + e.change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData") + }) + .map(|data| data.first().cloned()) +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 549bda78eda8..086a09b805c6 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -6,6 +6,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod ephemeral_key; pub mod events; @@ -68,6 +69,7 @@ pub trait StorageInterface: + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface + + dashboard_metadata::DashboardMetadataInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface + events::EventInterface diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs new file mode 100644 index 000000000000..2e8129398ca3 --- /dev/null +++ b/crates/router/src/db/dashboard_metadata.rs @@ -0,0 +1,184 @@ +use diesel_models::{enums, user::dashboard_metadata as storage}; +use error_stack::{IntoReport, ResultExt}; +use storage_impl::MockDb; + +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait DashboardMetadataInterface { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult; + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for Store { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + metadata + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_user_scoped_dashboard_metadata( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_merchant_scoped_dashboard_metadata( + &conn, + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for MockDb { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + if dashboard_metadata.iter().any(|metadata_inner| { + metadata_inner.user_id == metadata.user_id + && metadata_inner.merchant_id == metadata.merchant_id + && metadata_inner.org_id == metadata.org_id + && metadata_inner.data_key == metadata.data_key + }) { + Err(errors::StorageError::DuplicateValue { + entity: "user_id, merchant_id, org_id and data_key", + key: None, + })? + } + let metadata_new = storage::DashboardMetadata { + id: dashboard_metadata + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: metadata.user_id, + merchant_id: metadata.merchant_id, + org_id: metadata.org_id, + data_key: metadata.data_key, + data_value: metadata.data_value, + created_by: metadata.created_by, + created_at: metadata.created_at, + last_modified_by: metadata.last_modified_by, + last_modified_at: metadata.last_modified_at, + }; + dashboard_metadata.push(metadata_new.clone()); + Ok(metadata_new) + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner + .user_id + .clone() + .map(|user_id_inner| user_id_inner == user_id) + .unwrap_or(false) + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for user_id = {user_id},\ + merchant_id = {merchant_id}, org_id = {org_id} and data_keys = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner.user_id.is_none() + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for merchant_id = {merchant_id},\ + org_id = {org_id} and data_keyss = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 9cf1a7b80b8b..fcceba7fadba 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -6,6 +6,7 @@ use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; use diesel_models::{ + enums, enums::ProcessTrackerStatus, ephemeral_key::{EphemeralKey, EphemeralKeyNew}, reverse_lookup::{ReverseLookup, ReverseLookupNew}, @@ -21,7 +22,10 @@ use scheduler::{ use storage_impl::redis::kv_store::RedisConnInterface; use time::PrimitiveDateTime; -use super::{user::UserInterface, user_role::UserRoleInterface}; +use super::{ + dashboard_metadata::DashboardMetadataInterface, user::UserInterface, + user_role::UserRoleInterface, +}; use crate::{ core::errors::{self, ProcessTrackerError}, db::{ @@ -1915,3 +1919,35 @@ impl UserRoleInterface for KafkaStore { self.diesel_store.list_user_roles_by_user_id(user_id).await } } + +#[async_trait::async_trait] +impl DashboardMetadataInterface for KafkaStore { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_metadata(metadata).await + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys) + .await + } + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys) + .await + } +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2a7e1ab61905..2f8932057fb4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -807,6 +807,11 @@ impl User { .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) .service(web::resource("/change_password").route(web::post().to(change_password))) + .service( + web::resource("/data/merchant") + .route(web::post().to(set_merchant_scoped_dashboard_metadata)), + ) + .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c7369b9e4d52..72bc3c9cd417 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -147,9 +147,11 @@ impl From for ApiIdentifier { | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, - Flow::UserConnectAccount | Flow::ChangePassword | Flow::VerifyPaymentConnector => { - Self::User - } + Flow::UserConnectAccount + | Flow::ChangePassword + | Flow::SetDashboardMetadata + | Flow::GetMutltipleDashboardMetadata + | Flow::VerifyPaymentConnector => Self::User, } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 7d3d183eda76..3f5f7815ffbc 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -1,5 +1,6 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::user as user_api; +use api_models::{errors::types::ApiErrorResponse, user as user_api}; +use common_utils::errors::ReportSwitchExt; use router_env::Flow; use super::AppState; @@ -8,7 +9,9 @@ use crate::{ services::{ api, authentication::{self as auth}, + authorization::permissions::Permission, }, + utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; pub async fn user_connect_account( @@ -47,3 +50,55 @@ pub async fn change_password( )) .await } + +pub async fn set_merchant_scoped_dashboard_metadata( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SetDashboardMetadata; + let mut payload = json_payload.into_inner(); + + if let Err(e) = common_utils::errors::ReportSwitchExt::<(), ApiErrorResponse>::switch( + set_ip_address_if_required(&mut payload, req.headers()), + ) { + return api::log_and_return_error_response(e); + } + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user::dashboard_metadata::set_metadata, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_multiple_dashboard_metadata( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::GetMutltipleDashboardMetadata; + let payload = match ReportSwitchExt::<_, ApiErrorResponse>::switch(parse_string_to_enums( + query.into_inner().keys, + )) { + Ok(payload) => payload, + Err(e) => { + return api::log_and_return_error_response(e); + } + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user::dashboard_metadata::get_multiple_metadata, + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index c053b0f15448..7e723bf00c32 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -27,6 +27,8 @@ use crate::{ utils::user::password, }; +pub mod dashboard_metadata; + #[derive(Clone)] pub struct UserName(Secret); diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs new file mode 100644 index 000000000000..e65379346ac9 --- /dev/null +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -0,0 +1,56 @@ +use api_models::user::dashboard_metadata as api; +use diesel_models::enums::DashboardMetadata as DBEnum; +use masking::Secret; +use time::PrimitiveDateTime; + +pub enum MetaData { + ProductionAgreement(ProductionAgreementValue), + SetupProcessor(api::SetupProcessor), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(api::ProcessorConnected), + SecondProcessorConnected(api::ProcessorConnected), + ConfiguredRouting(api::ConfiguredRouting), + TestPayment(api::TestPayment), + IntegrationMethod(api::IntegrationMethod), + IntegrationCompleted(bool), + StripeConnected(api::ProcessorConnected), + PaypalConnected(api::ProcessorConnected), + SPRoutingConfigured(api::ConfiguredRouting), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} + +impl From<&MetaData> for DBEnum { + fn from(value: &MetaData) -> Self { + match value { + MetaData::ProductionAgreement(_) => Self::ProductionAgreement, + MetaData::SetupProcessor(_) => Self::SetupProcessor, + MetaData::ConfigureEndpoint(_) => Self::ConfigureEndpoint, + MetaData::SetupComplete(_) => Self::SetupComplete, + MetaData::FirstProcessorConnected(_) => Self::FirstProcessorConnected, + MetaData::SecondProcessorConnected(_) => Self::SecondProcessorConnected, + MetaData::ConfiguredRouting(_) => Self::ConfiguredRouting, + MetaData::TestPayment(_) => Self::TestPayment, + MetaData::IntegrationMethod(_) => Self::IntegrationMethod, + MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, + MetaData::StripeConnected(_) => Self::StripeConnected, + MetaData::PaypalConnected(_) => Self::PaypalConnected, + MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::SPTestPayment(_) => Self::SpTestPayment, + MetaData::DownloadWoocom(_) => Self::DownloadWoocom, + MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom, + MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook, + MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration, + } + } +} +#[derive(Debug, serde::Serialize)] +pub struct ProductionAgreementValue { + pub version: String, + pub ip_address: Secret, + pub timestamp: PrimitiveDateTime, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index e3e19323357b..a83a405f3554 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod enums; pub mod ephemeral_key; @@ -42,11 +43,11 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, - ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, - payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, + locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, + merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, + process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/dashboard_metadata.rs b/crates/router/src/types/storage/dashboard_metadata.rs new file mode 100644 index 000000000000..d804dfb1ff8b --- /dev/null +++ b/crates/router/src/types/storage/dashboard_metadata.rs @@ -0,0 +1 @@ +pub use diesel_models::user::dashboard_metadata::*; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c72e4b9feb3c..824f7f63af75 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1 +1,2 @@ +pub mod dashboard_metadata; pub mod password; diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs new file mode 100644 index 000000000000..5f354e613f95 --- /dev/null +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -0,0 +1,162 @@ +use std::{net::IpAddr, str::FromStr}; + +use actix_web::http::header::HeaderMap; +use api_models::user::dashboard_metadata::{ + GetMetaDataRequest, GetMultipleMetaDataPayload, SetMetaDataRequest, +}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; + +use crate::{ + core::errors::{UserErrors, UserResult}, + headers, AppState, +}; + +pub async fn insert_merchant_scoped_metadata_to_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let now = common_utils::date_time::now(); + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + state + .store + .insert_metadata(DashboardMetadataNew { + user_id: None, + merchant_id, + org_id, + data_key: metadata_key, + data_value, + created_by: user_id.clone(), + created_at: now, + last_modified_by: user_id, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + return e.change_context(UserErrors::MetadataAlreadySet); + } + e.change_context(UserErrors::InternalServerError) + }) +} + +pub async fn get_merchant_scoped_metadata_from_db( + state: &AppState, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_merchant_scoped_dashboard_metadata(&merchant_id, &org_id, metadata_keys) + .await + { + Ok(data) => Ok(data), + Err(e) => { + if e.current_context().is_db_not_found() { + return Ok(Vec::with_capacity(0)); + } + Err(e + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData")) + } + } +} + +pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResult> +where + T: serde::de::DeserializeOwned, +{ + data.map(|metadata| serde_json::from_value(metadata.data_value.clone())) + .transpose() + .map_err(|_| UserErrors::InternalServerError.into()) + .attach_printable("Error Serializing Metadata from DB") +} + +pub fn separate_metadata_type_based_on_scope( + metadata_keys: Vec, +) -> (Vec, Vec) { + let (mut merchant_scoped, user_scoped) = ( + Vec::with_capacity(metadata_keys.len()), + Vec::with_capacity(metadata_keys.len()), + ); + for key in metadata_keys { + match key { + DBEnum::ProductionAgreement + | DBEnum::SetupProcessor + | DBEnum::ConfigureEndpoint + | DBEnum::SetupComplete + | DBEnum::FirstProcessorConnected + | DBEnum::SecondProcessorConnected + | DBEnum::ConfiguredRouting + | DBEnum::TestPayment + | DBEnum::IntegrationMethod + | DBEnum::IntegrationCompleted + | DBEnum::StripeConnected + | DBEnum::PaypalConnected + | DBEnum::SpRoutingConfigured + | DBEnum::SpTestPayment + | DBEnum::DownloadWoocom + | DBEnum::ConfigureWoocom + | DBEnum::SetupWoocomWebhook + | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + } + } + (merchant_scoped, user_scoped) +} + +pub fn is_backfill_required(metadata_key: &DBEnum) -> bool { + matches!( + metadata_key, + DBEnum::StripeConnected | DBEnum::PaypalConnected + ) +} + +pub fn set_ip_address_if_required( + request: &mut SetMetaDataRequest, + headers: &HeaderMap, +) -> UserResult<()> { + if let SetMetaDataRequest::ProductionAgreement(req) = request { + let ip_address_from_request: Secret = headers + .get(headers::X_FORWARDED_FOR) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("X-Forwarded-For header not found")? + .to_str() + .map_err(|_| UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error converting Header Value to Str")? + .split(',') + .next() + .and_then(|ip| { + let ip_addr: Result = ip.parse(); + ip_addr.ok() + }) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error Parsing header value to ip")? + .to_string() + .into(); + req.ip_address = Some(ip_address_from_request) + } + Ok(()) +} + +pub fn parse_string_to_enums(query: String) -> UserResult { + Ok(GetMultipleMetaDataPayload { + results: query + .split(',') + .map(GetMetaDataRequest::from_str) + .collect::, _>>() + .map_err(|_| UserErrors::InvalidMetadataRequest.into()) + .attach_printable("Error Parsing to DashboardMetadata enums")?, + }) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index c254f89b4eef..7b87d2703640 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -259,6 +259,10 @@ pub enum Flow { DecisionManagerRetrieveConfig, /// Change password flow ChangePassword, + /// Set Dashboard Metadata flow + SetDashboardMetadata, + /// Get Multiple Dashboard Metadata flow + GetMutltipleDashboardMetadata, /// Payment Connector Verify VerifyPaymentConnector, } diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 4cdf8e2456bb..e22d39ce70c8 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -43,6 +43,7 @@ pub struct MockDb { pub organizations: Arc>>, pub users: Arc>>, pub user_roles: Arc>>, + pub dashboard_metadata: Arc>>, } impl MockDb { @@ -78,6 +79,7 @@ impl MockDb { organizations: Default::default(), users: Default::default(), user_roles: Default::default(), + dashboard_metadata: Default::default(), }) } } diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql new file mode 100644 index 000000000000..746fb42109e9 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX IF EXISTS dashboard_metadata_index; +DROP TABLE IF EXISTS dashboard_metadata; \ No newline at end of file diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql new file mode 100644 index 000000000000..8296f755f543 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS dashboard_metadata ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64), + merchant_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + data_key VARCHAR(64) NOT NULL, + data_value JSON NOT NULL, + created_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_by VARCHAR(64) NOT NULL, + last_modified_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (COALESCE(user_id,'0'), merchant_id, org_id, data_key); \ No newline at end of file From d30b58abb5e716b70c2dadec9e6f13c9e3403b6f Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:31:01 +0530 Subject: [PATCH 114/443] feat(connector): [BANKOFAMERICA] Add Required Fields for GPAY (#3014) --- crates/router/src/configs/defaults.rs | 219 ++++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 9 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index f5c3b46b27f2..f9bfcae1ca10 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -503,15 +503,6 @@ impl Default for super::settings::RequiredFields { value: None, } ), - ( - "payment_method_data.card.card_holder_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), - display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -2418,6 +2409,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Bluesnap, RequiredFieldFinal { @@ -4250,6 +4364,93 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ]), }, ), From c6cb527f07e23796c342f3562fbf3b61f1ef6801 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:41:59 +0530 Subject: [PATCH 115/443] fix(routing): Fix kgraph to exclude PM auth during construction (#3019) --- crates/router/src/core/payments/routing.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 841b48b9444a..96cd65615199 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -523,8 +523,10 @@ pub async fn refresh_kgraph_cache( .await .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; - merchant_connector_accounts - .retain(|mca| mca.connector_type != storage_enums::ConnectorType::PaymentVas); + merchant_connector_accounts.retain(|mca| { + mca.connector_type != storage_enums::ConnectorType::PaymentVas + && mca.connector_type != storage_enums::ConnectorType::PaymentMethodAuth + }); #[cfg(feature = "business_profile_routing")] let merchant_connector_accounts = payments_oss::helpers::filter_mca_based_on_business_profile( From 1e60c710985b341a118bb32962bd74b406d78f69 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 30 Nov 2023 16:50:18 +0530 Subject: [PATCH 116/443] refactor(postman): Fix payme postman collection for handling `order_details` (#2996) --- Cargo.lock | 11 ++++++++-- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../QuickStart/Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../payme.postman_collection.json | 20 +++++++++---------- 12 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 417e6d85db6d..e8719b29f51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3797,6 +3797,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + [[package]] name = "nanoid" version = "0.4.0" @@ -6832,11 +6838,12 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a512ec11fae6c666707625e84f83e5d58f941e9ab15723289c0d380edfe48f09" +checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" dependencies = [ "actix-web", + "mutually_exclusive_features", "opentelemetry", "pin-project", "tracing", diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 99392fc0f916..5a651cc0f119 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index 90982e5acd38..54cf1b15e3db 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -69,7 +69,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json index 0fc567f8bea0..f0915480e13e 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json index 625ae3a9d286..00b12f40997f 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario5-Refund full payment/Payments - Create/request.json @@ -78,7 +78,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json index 99392fc0f916..5a651cc0f119 100644 --- a/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json index a99d3db4fa53..72c62f360b8d 100644 --- a/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/QuickStart/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 0fc567f8bea0..f0915480e13e 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index a63210df7f42..03aea095ff35 100644 --- a/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/payme/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -79,7 +79,7 @@ { "product_name": "Apple iphone 15", "quantity": 1, - "amount": 5500, + "amount": 6540, "account_name": "transaction_processing" } ] diff --git a/postman/collection-json/payme.postman_collection.json b/postman/collection-json/payme.postman_collection.json index 4bca668a6af6..280a131386e5 100644 --- a/postman/collection-json/payme.postman_collection.json +++ b/postman/collection-json/payme.postman_collection.json @@ -532,7 +532,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -761,7 +761,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1003,7 +1003,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1395,7 +1395,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1787,7 +1787,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2189,7 +2189,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3364,7 +3364,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4506,7 +4506,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4886,7 +4886,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5147,7 +5147,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":5500,\"account_name\":\"transaction_processing\"}]}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\",\"last_name\":\"gnana\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"order_details\":[{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":6540,\"account_name\":\"transaction_processing\"}]}" }, "url": { "raw": "{{baseUrl}}/payments", From 1ca2ba459495ff9340954c87a6ae3e4dce0e7b71 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:56:34 +0530 Subject: [PATCH 117/443] feat(router): make core changes in payments flow to support incremental authorization (#3009) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- connector-template/transformers.rs | 1 + crates/api_models/src/payments.rs | 6 ++++ crates/common_enums/src/enums.rs | 24 +++++++++++++++ crates/data_models/src/payments.rs | 2 ++ .../src/payments/payment_intent.rs | 14 ++++++++- crates/diesel_models/src/enums.rs | 1 + crates/diesel_models/src/payment_intent.rs | 20 ++++++++++++- crates/diesel_models/src/schema.rs | 2 ++ .../router/src/connector/aci/transformers.rs | 1 + .../src/connector/adyen/transformers.rs | 8 +++++ .../src/connector/airwallex/transformers.rs | 2 ++ .../connector/authorizedotnet/transformers.rs | 3 ++ .../src/connector/bambora/transformers.rs | 2 ++ .../connector/bankofamerica/transformers.rs | 5 ++++ .../src/connector/bitpay/transformers.rs | 1 + crates/router/src/connector/bluesnap.rs | 1 + .../src/connector/bluesnap/transformers.rs | 1 + .../router/src/connector/boku/transformers.rs | 1 + .../braintree_graphql_transformers.rs | 9 ++++++ .../src/connector/braintree/transformers.rs | 1 + .../src/connector/cashtocode/transformers.rs | 2 ++ .../src/connector/checkout/transformers.rs | 4 +++ .../src/connector/coinbase/transformers.rs | 1 + .../src/connector/cryptopay/transformers.rs | 1 + .../src/connector/cybersource/transformers.rs | 19 ++++++++---- .../src/connector/dlocal/transformers.rs | 4 +++ .../connector/dummyconnector/transformers.rs | 1 + .../src/connector/fiserv/transformers.rs | 2 ++ .../src/connector/forte/transformers.rs | 4 +++ .../src/connector/globalpay/transformers.rs | 1 + .../src/connector/globepay/transformers.rs | 2 ++ .../src/connector/gocardless/transformers.rs | 3 ++ .../src/connector/helcim/transformers.rs | 5 ++++ .../src/connector/iatapay/transformers.rs | 2 ++ .../src/connector/klarna/transformers.rs | 1 + .../src/connector/mollie/transformers.rs | 1 + .../connector/multisafepay/transformers.rs | 1 + .../src/connector/nexinets/transformers.rs | 2 ++ .../router/src/connector/nmi/transformers.rs | 5 ++++ .../router/src/connector/noon/transformers.rs | 1 + .../src/connector/nuvei/transformers.rs | 1 + .../src/connector/opayo/transformers.rs | 1 + .../src/connector/opennode/transformers.rs | 1 + .../src/connector/payeezy/transformers.rs | 1 + .../src/connector/payme/transformers.rs | 3 ++ crates/router/src/connector/paypal.rs | 1 + .../src/connector/paypal/transformers.rs | 7 +++++ .../router/src/connector/payu/transformers.rs | 4 +++ .../src/connector/powertranz/transformers.rs | 1 + .../src/connector/prophetpay/transformers.rs | 4 +++ .../src/connector/rapyd/transformers.rs | 1 + .../src/connector/shift4/transformers.rs | 2 ++ .../src/connector/square/transformers.rs | 1 + .../router/src/connector/stax/transformers.rs | 1 + .../src/connector/stripe/transformers.rs | 4 +++ .../src/connector/trustpay/transformers.rs | 5 ++++ .../router/src/connector/tsys/transformers.rs | 2 ++ .../router/src/connector/volt/transformers.rs | 2 ++ .../src/connector/worldline/transformers.rs | 2 ++ crates/router/src/connector/worldpay.rs | 3 ++ .../src/connector/worldpay/transformers.rs | 1 + .../router/src/connector/zen/transformers.rs | 2 ++ crates/router/src/core/payments/helpers.rs | 9 ++++++ .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_confirm.rs | 9 ++++++ .../payments/operations/payment_create.rs | 8 +++++ .../payments/operations/payment_response.rs | 14 +++++++++ .../router/src/core/payments/transformers.rs | 18 +++++++++++ crates/router/src/core/utils.rs | 30 +++++++++++++++++++ crates/router/src/types.rs | 4 +++ .../router/src/types/api/verify_connector.rs | 1 + crates/router/src/workflows/payment_sync.rs | 2 +- crates/router/tests/connectors/aci.rs | 1 + crates/router/tests/connectors/adyen.rs | 1 + crates/router/tests/connectors/bitpay.rs | 1 + crates/router/tests/connectors/cashtocode.rs | 1 + crates/router/tests/connectors/coinbase.rs | 1 + crates/router/tests/connectors/cryptopay.rs | 1 + crates/router/tests/connectors/opennode.rs | 1 + crates/router/tests/connectors/utils.rs | 2 ++ crates/router/tests/connectors/worldline.rs | 1 + .../src/mock_db/payment_intent.rs | 2 ++ .../src/payments/payment_intent.rs | 24 +++++++++++++-- .../down.sql | 3 ++ .../up.sql | 3 ++ .../down.sql | 2 ++ .../up.sql | 2 ++ openapi/openapi_spec.json | 15 ++++++++++ 88 files changed, 363 insertions(+), 11 deletions(-) create mode 100644 migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql create mode 100644 migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql create mode 100644 migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql create mode 100644 migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql diff --git a/connector-template/transformers.rs b/connector-template/transformers.rs index 3ed53a906a2e..bdbfb2e45672 100644 --- a/connector-template/transformers.rs +++ b/connector-template/transformers.rs @@ -130,6 +130,7 @@ impl TryFrom)] pub payment_type: Option, + + ///Request for an incremental authorization + pub request_incremental_authorization: Option, } impl PaymentsRequest { @@ -2210,6 +2213,9 @@ pub struct PaymentsResponse { /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment pub merchant_connector_id: Option, + + /// If true incremental authorization can be performed on this payment + pub incremental_authorization_allowed: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 3f343965130e..8da4a2da54cc 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -12,6 +12,7 @@ pub mod diesel_exports { DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, }; } @@ -1387,6 +1388,29 @@ pub enum CountryAlpha2 { US } +#[derive( + Clone, + Debug, + Copy, + Default, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RequestIncrementalAuthorization { + True, + False, + #[default] + Default, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[rustfmt::skip] pub enum CountryAlpha3 { diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index 4e7a0923f6a9..af2076bfa10d 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -50,4 +50,6 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 2c5914f5b37f..d8f927a4e2c5 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -107,6 +107,8 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -116,6 +118,7 @@ pub enum PaymentIntentUpdate { amount_captured: Option, return_url: Option, updated_by: String, + incremental_authorization_allowed: Option, }, MetadataUpdate { metadata: pii::SecretSerdeValue, @@ -137,6 +140,7 @@ pub enum PaymentIntentUpdate { }, PGStatusUpdate { status: storage_enums::IntentStatus, + incremental_authorization_allowed: Option, updated_by: String, }, Update { @@ -213,6 +217,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, } impl From for PaymentIntentUpdateInternal { @@ -283,10 +288,15 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::PGStatusUpdate { status, updated_by } => Self { + PaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => Self { status: Some(status), modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -310,6 +320,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -319,6 +330,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 3ddd85f37891..3f8b37cd03f7 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -15,6 +15,7 @@ pub mod diesel_exports { DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoutingAlgorithmKind as RoutingAlgorithmKind, }; } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index b6ff4fcf8d8d..8d752466103e 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -1,3 +1,4 @@ +use common_enums::RequestIncrementalAuthorization; use common_utils::pii; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; @@ -51,6 +52,8 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive( @@ -106,6 +109,8 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, + pub request_incremental_authorization: RequestIncrementalAuthorization, + pub incremental_authorization_allowed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,6 +120,7 @@ pub enum PaymentIntentUpdate { amount_captured: Option, return_url: Option, updated_by: String, + incremental_authorization_allowed: Option, }, MetadataUpdate { metadata: pii::SecretSerdeValue, @@ -137,6 +143,7 @@ pub enum PaymentIntentUpdate { PGStatusUpdate { status: storage_enums::IntentStatus, updated_by: String, + incremental_authorization_allowed: Option, }, Update { amount: i64, @@ -213,6 +220,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, + pub incremental_authorization_allowed: Option, } impl PaymentIntentUpdate { @@ -243,6 +251,7 @@ impl PaymentIntentUpdate { payment_confirm_source, updated_by, surcharge_applicable, + incremental_authorization_allowed, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -272,6 +281,8 @@ impl PaymentIntentUpdate { payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), updated_by, surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), + + incremental_authorization_allowed, ..source } } @@ -345,10 +356,15 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, - PaymentIntentUpdate::PGStatusUpdate { status, updated_by } => Self { + PaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => Self { status: Some(status), modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::MerchantStatusUpdate { @@ -372,6 +388,7 @@ impl From for PaymentIntentUpdateInternal { // customer_id, return_url, updated_by, + incremental_authorization_allowed, } => Self { // amount, // currency: Some(currency), @@ -381,6 +398,7 @@ impl From for PaymentIntentUpdateInternal { return_url, modified_at: Some(common_utils::date_time::now()), updated_by, + incremental_authorization_allowed, ..Default::default() }, PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 6cab6d5730d0..13b001ecc6d1 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -678,6 +678,8 @@ diesel::table! { #[max_length = 32] updated_by -> Varchar, surcharge_applicable -> Nullable, + request_incremental_authorization -> RequestIncrementalAuthorization, + incremental_authorization_allowed -> Nullable, } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 66aeb3bb6b2b..9cfb657bdca8 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -733,6 +733,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 4b3fcc851323..1793e3e07a87 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2978,6 +2978,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -3011,6 +3012,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), payment_method_balance: Some(types::PaymentMethodBalance { amount: item.response.balance.value, @@ -3072,6 +3074,7 @@ pub fn get_adyen_response( connector_metadata: None, network_txn_id, connector_response_reference_id: Some(response.merchant_reference), + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3171,6 +3174,7 @@ pub fn get_redirection_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3222,6 +3226,7 @@ pub fn get_present_to_shopper_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3270,6 +3275,7 @@ pub fn get_qr_code_response( connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) } @@ -3304,6 +3310,7 @@ pub fn get_redirection_error_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }; Ok((status, error, payments_response_data)) @@ -3638,6 +3645,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount.value), ..item.data diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 3785e02d4747..2de7f6fe00ff 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -555,6 +555,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -596,6 +597,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 2c8a63a53e5c..30323ca4ef23 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -610,6 +610,7 @@ impl connector_response_reference_id: Some( transaction_response.transaction_id.clone(), ), + incremental_authorization_allowed: None, }), }, ..item.data @@ -680,6 +681,7 @@ impl connector_response_reference_id: Some( transaction_response.transaction_id.clone(), ), + incremental_authorization_allowed: None, }), }, ..item.data @@ -977,6 +979,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(transaction.transaction_id.clone()), + incremental_authorization_allowed: None, }), status: payment_status, ..item.data diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index e686186c901b..2d50569f9a49 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -215,6 +215,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(pg_response.order_number.to_string()), + incremental_authorization_allowed: None, }), ..item.data }), @@ -241,6 +242,7 @@ impl connector_response_reference_id: Some( item.data.connector_request_reference_id.to_string(), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 12170deb1a00..18ec8ceb89d9 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -528,6 +528,7 @@ impl .code .unwrap_or(info_response.id), ), + incremental_authorization_allowed: None, }), ..item.data }), @@ -585,6 +586,7 @@ impl .code .unwrap_or(info_response.id), ), + incremental_authorization_allowed: None, }), ..item.data }), @@ -642,6 +644,7 @@ impl .code .unwrap_or(info_response.id), ), + incremental_authorization_allowed: None, }), ..item.data }), @@ -719,6 +722,7 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(app_response.id)), + incremental_authorization_allowed: None, }), ..item.data }), @@ -733,6 +737,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(error_response.id), + incremental_authorization_allowed: None, }), ..item.data }), diff --git a/crates/router/src/connector/bitpay/transformers.rs b/crates/router/src/connector/bitpay/transformers.rs index 89dd2368b2b7..0ddf2dbf913b 100644 --- a/crates/router/src/connector/bitpay/transformers.rs +++ b/crates/router/src/connector/bitpay/transformers.rs @@ -178,6 +178,7 @@ impl .data .order_id .or(Some(item.response.data.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 0bc56d4e9955..25cdcb731f11 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -713,6 +713,7 @@ impl ConnectorIntegration connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/boku/transformers.rs b/crates/router/src/connector/boku/transformers.rs index 3df9126fc4c0..c671560765d0 100644 --- a/crates/router/src/connector/boku/transformers.rs +++ b/crates/router/src/connector/boku/transformers.rs @@ -252,6 +252,7 @@ impl TryFrom connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -272,6 +273,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -435,6 +437,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -452,6 +455,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), @@ -495,6 +499,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -539,6 +544,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1061,6 +1067,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1158,6 +1165,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1255,6 +1263,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index dcca9c26434c..44daef94e8a6 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -239,6 +239,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index cfca998e06c3..b38ca4b67132 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -238,6 +238,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } @@ -281,6 +282,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: Some(item.response.amount), ..item.data diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 173ac0b8f585..ebe02f30d5ff 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -591,6 +591,7 @@ impl TryFrom> connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -640,6 +641,7 @@ impl TryFrom> connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }; Ok(Self { status, @@ -714,6 +716,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: response.into(), ..item.data @@ -810,6 +813,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.reference, + incremental_authorization_allowed: None, }), status, amount_captured, diff --git a/crates/router/src/connector/coinbase/transformers.rs b/crates/router/src/connector/coinbase/transformers.rs index 6cc097bc9d8d..ce9bb3e871c5 100644 --- a/crates/router/src/connector/coinbase/transformers.rs +++ b/crates/router/src/connector/coinbase/transformers.rs @@ -146,6 +146,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.data.id.clone()), + incremental_authorization_allowed: None, }), |context| { Ok(types::PaymentsResponseData::TransactionUnresolvedResponse{ diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 446da0761d1f..3af604c786b8 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -173,6 +173,7 @@ impl .data .custom_id .or(Some(item.response.data.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 81df29966725..495e23e001ad 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -554,8 +554,9 @@ impl connector_mandate_id: Some(token_info.instrument_identifier.id), payment_method_id: None, }); + let status = get_payment_status(is_capture, item.response.status.into()); Ok(Self { - status: get_payment_status(is_capture, item.response.status.into()), + status, response: match item.response.error_information { Some(error) => Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), @@ -578,6 +579,9 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some( + status == enums::AttemptStatus::Authorized, + ), }), }, ..item.data @@ -640,6 +644,9 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some( + mandate_status == enums::AttemptStatus::Authorized, + ), }), }, ..item.data @@ -694,11 +701,12 @@ impl ) -> Result { let item = data.0; let is_capture = data.1; + let status = get_payment_status( + is_capture, + item.response.application_information.status.into(), + ); Ok(Self { - status: get_payment_status( - is_capture, - item.response.application_information.status.into(), - ), + status, response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data: None, @@ -710,6 +718,7 @@ impl .client_reference_information .map(|cref| cref.code) .unwrap_or(Some(item.response.id)), + incremental_authorization_allowed: Some(status == enums::AttemptStatus::Authorized), }), ..item.data }) diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index f7cfa6a868bd..92d01cfe56d4 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -329,6 +329,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }; Ok(Self { status: enums::AttemptStatus::from(item.response.status), @@ -368,6 +369,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }), ..item.data }) @@ -404,6 +406,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.order_id.clone(), + incremental_authorization_allowed: None, }), ..item.data }) @@ -440,6 +443,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_id.clone()), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs index dc707bde42cc..3c7bd2e09d9a 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -250,6 +250,7 @@ impl TryFrom connector_response_reference_id: Some( gateway_resp.transaction_processing_details.order_id, ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -403,6 +404,7 @@ impl TryFrom })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) @@ -324,6 +325,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) @@ -391,6 +393,7 @@ impl TryFrom> })), network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id.to_string()), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -458,6 +461,7 @@ impl })), network_txn_id: None, connector_response_reference_id: Some(transaction_id.to_string()), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 78a83e700267..9cef564b3795 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -234,6 +234,7 @@ fn get_payment_response( connector_metadata: None, network_txn_id: None, connector_response_reference_id: response.reference, + incremental_authorization_allowed: None, }), } } diff --git a/crates/router/src/connector/globepay/transformers.rs b/crates/router/src/connector/globepay/transformers.rs index ef23f48f5197..f6adacb814de 100644 --- a/crates/router/src/connector/globepay/transformers.rs +++ b/crates/router/src/connector/globepay/transformers.rs @@ -157,6 +157,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -230,6 +231,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index 63e199657af0..249dae370b1a 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -577,6 +577,7 @@ impl response: Ok(types::PaymentsResponseData::TransactionResponse { connector_metadata: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, resource_id: ResponseId::NoResponseId, redirection_data: None, mandate_reference, @@ -732,6 +733,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -766,6 +768,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 9f405e2e2ea1..dc38b2eeb253 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -328,6 +328,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -382,6 +383,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -440,6 +442,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -526,6 +529,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data @@ -588,6 +592,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), ..item.data diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 7cdfafc858b6..b6d2dee4a01b 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -286,6 +286,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, }), |checkout_methods| { Ok(types::PaymentsResponseData::TransactionResponse { @@ -299,6 +300,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, }) }, ), diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 563410ee99d0..0816dd82ec6b 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -167,6 +167,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_id.clone()), + incremental_authorization_allowed: None, }), status: item.response.fraud_status.into(), ..item.data diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index b77077ae709f..62fb94e236a8 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -531,6 +531,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 1780b77379c7..7672566f8274 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -694,6 +694,7 @@ impl connector_response_reference_id: Some( payment_response.data.order_id.clone(), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 15cbe9a7e28e..8875abdb7868 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -372,6 +372,7 @@ impl connector_metadata: Some(connector_metadata), network_txn_id: None, connector_response_reference_id: Some(item.response.order_id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -455,6 +456,7 @@ impl connector_metadata: Some(connector_metadata), network_txn_id: None, connector_response_reference_id: Some(item.response.order.order_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index ff3a1e6a1c54..35c0e102020e 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -322,6 +322,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::CaptureInitiated, ), @@ -415,6 +416,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::Charged, ), @@ -470,6 +472,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), if let Some(diesel_models::enums::CaptureMethod::Automatic) = item.data.request.capture_method @@ -519,6 +522,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), enums::AttemptStatus::VoidInitiated, ), @@ -570,6 +574,7 @@ impl TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index ee3a8ba8c532..b478d63e0f12 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -527,6 +527,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id, + incremental_authorization_allowed: None, }) } }, diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 36244b8bc0d8..73e039c63395 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1452,6 +1452,7 @@ where }, network_txn_id: None, connector_response_reference_id: response.order_id, + incremental_authorization_allowed: None, }) }, ..item.data diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 5e9fb066c78d..7b633f6aa641 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -123,6 +123,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/opennode/transformers.rs b/crates/router/src/connector/opennode/transformers.rs index 794fc8573417..7670166fabaf 100644 --- a/crates/router/src/connector/opennode/transformers.rs +++ b/crates/router/src/connector/opennode/transformers.rs @@ -150,6 +150,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.data.order_id, + incremental_authorization_allowed: None, }) } else { Ok(types::PaymentsResponseData::TransactionUnresolvedResponse { diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 90c58c3a9bce..0170d18ecb46 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -440,6 +440,7 @@ impl .reference .unwrap_or(item.response.transaction_id), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index e751de20e219..e3d54881f1f2 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -262,6 +262,7 @@ impl TryFrom<&PaymePaySaleResponse> for types::PaymentsResponseData { ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }) } } @@ -326,6 +327,7 @@ impl From<&SaleQuery> for types::PaymentsResponseData { connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, } } } @@ -535,6 +537,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }), diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 9ab19b295570..c60b20bb367d 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -615,6 +615,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..data.clone() }) diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 04328cead233..fbe6a47d2007 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1174,6 +1174,7 @@ impl .invoice_id .clone() .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1278,6 +1279,7 @@ impl connector_response_reference_id: Some( purchase_units.map_or(item.response.id, |item| item.invoice_id.clone()), ), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1314,6 +1316,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1363,6 +1366,7 @@ impl connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -1430,6 +1434,7 @@ impl .invoice_id .clone() .or(Some(item.response.supplementary_data.related_ids.order_id)), + incremental_authorization_allowed: None, }), ..item.data }) @@ -1531,6 +1536,7 @@ impl TryFrom> .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), amount_captured: Some(amount_captured), ..item.data @@ -1581,6 +1587,7 @@ impl .response .invoice_id .or(Some(item.response.id)), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/payu/transformers.rs b/crates/router/src/connector/payu/transformers.rs index 9a2e14215c75..6edc570eb451 100644 --- a/crates/router/src/connector/payu/transformers.rs +++ b/crates/router/src/connector/payu/transformers.rs @@ -205,6 +205,7 @@ impl .response .ext_order_id .or(Some(item.response.order_id)), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -257,6 +258,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -342,6 +344,7 @@ impl .response .ext_order_id .or(Some(item.response.order_id)), + incremental_authorization_allowed: None, }), amount_captured: None, ..item.data @@ -475,6 +478,7 @@ impl .ext_order_id .clone() .or(Some(order.order_id.clone())), + incremental_authorization_allowed: None, }), amount_captured: Some( order diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index a631a126ed3f..e0ecd81c7e58 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -328,6 +328,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.order_identifier), + incremental_authorization_allowed: None, }), Err, ); diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index d81b931edfc9..d05f2c3986a7 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -219,6 +219,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -407,6 +408,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -456,6 +458,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -505,6 +508,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 898b6ed6d147..193eb8198926 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -487,6 +487,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ) } diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index c272a5b6fc12..606da2129fb0 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -702,6 +702,7 @@ impl ), network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) @@ -743,6 +744,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 6024a20fa6ab..7343ef58bb08 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -401,6 +401,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: item.response.payment.reference_id, + incremental_authorization_allowed: None, }), amount_captured, ..item.data diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 5aa0949a09cc..2fd3b3474ea4 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -367,6 +367,7 @@ impl connector_response_reference_id: Some( item.response.idempotency_id.unwrap_or(item.response.id), ), + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index ae7fe59be96c..182479604539 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -2334,6 +2334,7 @@ impl connector_metadata, network_txn_id, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), amount_captured: item.response.amount_received, ..item.data @@ -2494,6 +2495,7 @@ impl connector_metadata, network_txn_id: None, connector_response_reference_id: Some(item.response.id.clone()), + incremental_authorization_allowed: None, }), Err, ); @@ -2535,6 +2537,7 @@ impl connector_metadata: None, network_txn_id: Option::foreign_from(item.response.latest_attempt), connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -3076,6 +3079,7 @@ impl TryFrom types::PaymentsRes connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(connector_response.transaction_id), + incremental_authorization_allowed: None, } } @@ -241,6 +242,7 @@ fn get_payments_sync_response( .transaction_id .clone(), ), + incremental_authorization_allowed: None, } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index 6f4c67dce8a3..cea56feb7145 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -284,6 +284,7 @@ impl connector_metadata: None, network_txn_id: None, connector_response_reference_id: Some(item.response.id), + incremental_authorization_allowed: None, }), ..item.data }) @@ -335,6 +336,7 @@ impl TryFrom TryFrom TryFrom> connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }), ..item.data }) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 64f6d5bf1a07..c66b098fe751 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -940,6 +940,7 @@ impl TryFrom TryFrom let payment_intent_update = storage::PaymentIntentUpdate::PGStatusUpdate { status: enums::IntentStatus::Cancelled, updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: None, }; (Some(payment_intent_update), enums::AttemptStatus::Voided) } else { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 28b6dbec96ab..d718db79a6d0 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -419,6 +419,15 @@ impl .attach_printable("Error converting feature_metadata to Value")? .or(payment_intent.feature_metadata); payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); + payment_intent.request_incremental_authorization = request + .request_incremental_authorization + .map(|request_incremental_authorization| { + core_utils::get_request_incremental_authorization_value( + Some(request_incremental_authorization), + payment_attempt.capture_method, + ) + }) + .unwrap_or(Ok(payment_intent.request_incremental_authorization))?; payment_attempt.business_sub_label = request .business_sub_label .clone() diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index c12f28e23390..ac387076d1d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -713,6 +713,12 @@ impl PaymentCreate { let payment_link_id = payment_link_data.map(|pl_data| pl_data.payment_link_id); + let request_incremental_authorization = + core_utils::get_request_incremental_authorization_value( + request.request_incremental_authorization, + request.capture_method, + )?; + Ok(storage::PaymentIntentNew { payment_id: payment_id.to_string(), merchant_id: merchant_account.merchant_id.to_string(), @@ -749,6 +755,8 @@ impl PaymentCreate { payment_confirm_source: None, surcharge_applicable: None, updated_by: merchant_account.storage_scheme.to_string(), + request_incremental_authorization, + incremental_authorization_allowed: None, }) } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 2de5df38dba4..9781ad651ee2 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -418,8 +418,18 @@ async fn payment_response_update_tracker( redirection_data, connector_metadata, connector_response_reference_id, + incremental_authorization_allowed, .. } => { + payment_data + .payment_intent + .incremental_authorization_allowed = + core_utils::get_incremental_authorization_allowed_value( + incremental_authorization_allowed, + payment_data + .payment_intent + .request_incremental_authorization, + ); let connector_transaction_id = match resource_id { types::ResponseId::NoResponseId => None, types::ResponseId::ConnectorTransactionId(id) @@ -627,6 +637,7 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.status, ), updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: Some(false), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { status: api_models::enums::IntentStatus::foreign_from( @@ -635,6 +646,9 @@ async fn payment_response_update_tracker( return_url: router_data.return_url.clone(), amount_captured, updated_by: storage_scheme.to_string(), + incremental_authorization_allowed: payment_data + .payment_intent + .incremental_authorization_allowed, }, }; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 000bbb0fc00b..51e139c97988 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,6 +1,7 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr}; use api_models::payments::{FrmMessage, RequestSurchargeDetails}; +use common_enums::RequestIncrementalAuthorization; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; use error_stack::{IntoReport, ResultExt}; @@ -80,6 +81,7 @@ where connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, + incremental_authorization_allowed: None, }); let additional_data = PaymentAdditionalData { @@ -687,6 +689,9 @@ where .set_merchant_connector_id(payment_attempt.merchant_connector_id) .set_unified_code(payment_attempt.unified_code) .set_unified_message(payment_attempt.unified_message) + .set_incremental_authorization_allowed( + payment_intent.incremental_authorization_allowed, + ) .to_owned(), headers, )) @@ -749,6 +754,7 @@ where surcharge_details, unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, + incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, ..Default::default() }, headers, @@ -1036,6 +1042,12 @@ impl TryFrom> for types::PaymentsAuthoriz complete_authorize_url, customer_id: None, surcharge_details: payment_data.surcharge_details, + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), }) } } @@ -1274,6 +1286,12 @@ impl TryFrom> for types::SetupMandateRequ return_url: payment_data.payment_intent.return_url, browser_info, payment_method_type: attempt.payment_method_type, + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), }) } } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 5207e4ba8079..670c25c814ed 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,6 +4,7 @@ use api_models::{ enums::{DisputeStage, DisputeStatus}, payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, }; +use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; use common_utils::{ @@ -1133,3 +1134,32 @@ pub async fn get_individual_surcharge_detail_from_redis( .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") .await } + +pub fn get_request_incremental_authorization_value( + request_incremental_authorization: Option, + capture_method: Option, +) -> RouterResult { + request_incremental_authorization + .map(|request_incremental_authorization| { + if request_incremental_authorization { + if capture_method == Some(common_enums::CaptureMethod::Automatic) { + Err(errors::ApiErrorResponse::NotSupported { message: "incremental authorization is not supported when capture_method is automatic".to_owned() }).into_report()? + } + Ok(RequestIncrementalAuthorization::True) + } else { + Ok(RequestIncrementalAuthorization::False) + } + }) + .unwrap_or(Ok(RequestIncrementalAuthorization::default())) +} + +pub fn get_incremental_authorization_allowed_value( + incremental_authorization_allowed: Option, + request_incremental_authorization: RequestIncrementalAuthorization, +) -> Option { + if request_incremental_authorization == common_enums::RequestIncrementalAuthorization::False { + Some(false) + } else { + incremental_authorization_allowed + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c3118f0c05be..c267a54cc57b 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -381,6 +381,7 @@ pub struct PaymentsAuthorizeData { pub payment_method_type: Option, pub surcharge_details: Option, pub customer_id: Option, + pub request_incremental_authorization: bool, } #[derive(Debug, Clone, Default)] @@ -536,6 +537,7 @@ pub struct SetupMandateRequestData { pub email: Option, pub return_url: Option, pub payment_method_type: Option, + pub request_incremental_authorization: bool, } #[derive(Debug, Clone)] @@ -669,6 +671,7 @@ pub enum PaymentsResponseData { connector_metadata: Option, network_txn_id: Option, connector_response_reference_id: Option, + incremental_authorization_allowed: Option, }, MultipleCaptureResponse { // pending_capture_id_list: Vec, @@ -1200,6 +1203,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { payment_method_type: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: data.request.request_incremental_authorization, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 3e3511ccb98f..74b15f911b9a 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -47,6 +47,7 @@ impl VerifyConnectorData { complete_authorize_url: None, related_transaction_id: None, statement_descriptor_suffix: None, + request_incremental_authorization: false, } } diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index f2760a00582d..43567ce27e23 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -124,7 +124,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { .as_ref() .is_none() { - let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string() }; + let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed,updated_by: merchant_account.storage_scheme.to_string(), incremental_authorization_allowed: Some(false) }; let payment_attempt_update = data_models::payments::payment_attempt::PaymentAttemptUpdate::ErrorUpdate { connector: None, diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index e12e27708f87..7ddc504956fb 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -69,6 +69,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 4b2cbcb7c4a9..714dc0d7d672 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -157,6 +157,7 @@ impl AdyenTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 755427140c4f..3c9f08bf1b69 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index 871677bb692a..a7c95936fbe8 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -67,6 +67,7 @@ impl CashtocodeTest { complete_authorize_url: None, customer_id: Some("John Doe".to_owned()), surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 512e03a5c94d..2ddb5464d4df 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -94,6 +94,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index e9c43cee3af6..11e556215c35 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 248bbb02e520..707192e01c3b 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -93,6 +93,7 @@ fn payment_method_details() -> Option { capture_method: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index f325370e737f..823b3eae497d 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -908,6 +908,7 @@ impl Default for PaymentAuthorizeType { webhook_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }; Self(data) } @@ -1043,6 +1044,7 @@ pub fn get_connector_metadata( connector_metadata, network_txn_id: _, connector_response_reference_id: _, + incremental_authorization_allowed: _, }) => connector_metadata, _ => None, } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 6163949c6c58..fd697f95b754 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -102,6 +102,7 @@ impl WorldlineTest { complete_authorize_url: None, customer_id: None, surcharge_details: None, + request_incremental_authorization: false, }) } } diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index 08a4a2aabeaa..a3e82c1d1044 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -106,6 +106,8 @@ impl PaymentIntentInterface for MockDb { payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), surcharge_applicable: new.surcharge_applicable, + request_incremental_authorization: new.request_incremental_authorization, + incremental_authorization_allowed: new.incremental_authorization_allowed, }; payment_intents.push(payment_intent.clone()); Ok(payment_intent) diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index c3b3d22ffe35..fdf9875bc1ff 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -97,6 +97,8 @@ impl PaymentIntentInterface for KVRouterStore { payment_confirm_source: new.payment_confirm_source, updated_by: storage_scheme.to_string(), surcharge_applicable: new.surcharge_applicable, + request_incremental_authorization: new.request_incremental_authorization, + incremental_authorization_allowed: new.incremental_authorization_allowed, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -758,6 +760,8 @@ impl DataModelExt for PaymentIntentNew { payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, surcharge_applicable: self.surcharge_applicable, + request_incremental_authorization: self.request_incremental_authorization, + incremental_authorization_allowed: self.incremental_authorization_allowed, } } @@ -798,6 +802,8 @@ impl DataModelExt for PaymentIntentNew { payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, } } } @@ -843,6 +849,8 @@ impl DataModelExt for PaymentIntent { payment_confirm_source: self.payment_confirm_source, updated_by: self.updated_by, surcharge_applicable: self.surcharge_applicable, + request_incremental_authorization: self.request_incremental_authorization, + incremental_authorization_allowed: self.incremental_authorization_allowed, } } @@ -884,6 +892,8 @@ impl DataModelExt for PaymentIntent { payment_confirm_source: storage_model.payment_confirm_source, updated_by: storage_model.updated_by, surcharge_applicable: storage_model.surcharge_applicable, + request_incremental_authorization: storage_model.request_incremental_authorization, + incremental_authorization_allowed: storage_model.incremental_authorization_allowed, } } } @@ -898,11 +908,13 @@ impl DataModelExt for PaymentIntentUpdate { amount_captured, return_url, updated_by, + incremental_authorization_allowed, } => DieselPaymentIntentUpdate::ResponseUpdate { status, amount_captured, return_url, updated_by, + incremental_authorization_allowed, }, Self::MetadataUpdate { metadata, @@ -937,9 +949,15 @@ impl DataModelExt for PaymentIntentUpdate { billing_address_id, updated_by, }, - Self::PGStatusUpdate { status, updated_by } => { - DieselPaymentIntentUpdate::PGStatusUpdate { status, updated_by } - } + Self::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + } => DieselPaymentIntentUpdate::PGStatusUpdate { + status, + updated_by, + incremental_authorization_allowed, + }, Self::Update { amount, currency, diff --git a/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql new file mode 100644 index 000000000000..5ee12132dee6 --- /dev/null +++ b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS request_incremental_authorization; +DROP TYPE "RequestIncrementalAuthorization"; diff --git a/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql new file mode 100644 index 000000000000..2c4d68593588 --- /dev/null +++ b/migrations/2023-11-28-081058_add-request_incremental_authorization-in-payment-intent/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +CREATE TYPE "RequestIncrementalAuthorization" AS ENUM ('true', 'false', 'default'); +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS request_incremental_authorization "RequestIncrementalAuthorization" NOT NULL DEFAULT 'false'::"RequestIncrementalAuthorization"; diff --git a/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql new file mode 100644 index 000000000000..f08165481889 --- /dev/null +++ b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS incremental_authorization_allowed; \ No newline at end of file diff --git a/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql new file mode 100644 index 000000000000..73fe22dd52df --- /dev/null +++ b/migrations/2023-11-29-063030_add-incremental_authorization_allowed-in-payment-intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS incremental_authorization_allowed BOOLEAN; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 86dc053d2d77..f5ad99f05752 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9721,6 +9721,11 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true } } }, @@ -10085,6 +10090,11 @@ } ], "nullable": true + }, + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true } } }, @@ -10518,6 +10528,11 @@ "type": "string", "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", "nullable": true + }, + "incremental_authorization_allowed": { + "type": "boolean", + "description": "If true incremental authorization can be performed on this payment", + "nullable": true } } }, From 8c37a8d857c5a58872fa2b2e194b85e755129677 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 30 Nov 2023 18:51:51 +0530 Subject: [PATCH 118/443] fix(connector): [Trustpay] Add mapping to error code `800.100.165` and `900.100.100` (#2925) --- crates/router/src/connector/trustpay/transformers.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 5f3fb865d33d..e985eff11976 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -499,6 +499,7 @@ fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { true, "Transaction declined (maximum transaction frequency exceeded)", ), + "800.100.165" => (true, "Transaction declined (card lost)"), "800.100.168" => (true, "Transaction declined (restricted card)"), "800.100.170" => (true, "Transaction declined (transaction not permitted)"), "800.100.171" => (true, "transaction declined (pick up card)"), @@ -512,6 +513,10 @@ fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { true, "Transaction for the same session is currently being processed, please try again later", ), + "900.100.100" => ( + true, + "Unexpected communication error with connector/acquirer", + ), "900.100.300" => (true, "Timeout, uncertain result"), _ => (false, ""), } From 2e2dbe47156695beff6c0e4c800c0036fc426ed0 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:33:36 +0000 Subject: [PATCH 119/443] chore(version): v1.93.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2966b238bba..3831e3d1caf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.93.0 (2023-11-30) + +### Features + +- **connector:** [BANKOFAMERICA] Add Required Fields for GPAY ([#3014](https://github.com/juspay/hyperswitch/pull/3014)) ([`d30b58a`](https://github.com/juspay/hyperswitch/commit/d30b58abb5e716b70c2dadec9e6f13c9e3403b6f)) +- **core:** Add ability to verify connector credentials before integrating the connector ([#2986](https://github.com/juspay/hyperswitch/pull/2986)) ([`39f255b`](https://github.com/juspay/hyperswitch/commit/39f255b4b209588dec35d780078c2ab7ceb37b10)) +- **router:** Make core changes in payments flow to support incremental authorization ([#3009](https://github.com/juspay/hyperswitch/pull/3009)) ([`1ca2ba4`](https://github.com/juspay/hyperswitch/commit/1ca2ba459495ff9340954c87a6ae3e4dce0e7b71)) +- **user:** Add support for dashboard metadata ([#3000](https://github.com/juspay/hyperswitch/pull/3000)) ([`6a2e4ab`](https://github.com/juspay/hyperswitch/commit/6a2e4ab4169820f35e953a949bd2e82e7f098ed2)) + +### Bug Fixes + +- **connector:** + - Move authorised status to charged in setup mandate ([#3017](https://github.com/juspay/hyperswitch/pull/3017)) ([`663754d`](https://github.com/juspay/hyperswitch/commit/663754d629d59a17ba9d4985fe04f9404ceb16b7)) + - [Trustpay] Add mapping to error code `800.100.165` and `900.100.100` ([#2925](https://github.com/juspay/hyperswitch/pull/2925)) ([`8c37a8d`](https://github.com/juspay/hyperswitch/commit/8c37a8d857c5a58872fa2b2e194b85e755129677)) +- **core:** Error message on Refund update for `Not Implemented` Case ([#3011](https://github.com/juspay/hyperswitch/pull/3011)) ([`6b7ada1`](https://github.com/juspay/hyperswitch/commit/6b7ada1a34450ea3a7fc019375ba462a14ddd6ab)) +- **pm_list:** [Trustpay] Update Cards, Bank_redirect - blik pm type required field info for Trustpay ([#2999](https://github.com/juspay/hyperswitch/pull/2999)) ([`c05432c`](https://github.com/juspay/hyperswitch/commit/c05432c0bd70f222c2f898ce2cbb47a46364a490)) +- **router:** + - [Dlocal] connector transaction id fix ([#2872](https://github.com/juspay/hyperswitch/pull/2872)) ([`44b1f49`](https://github.com/juspay/hyperswitch/commit/44b1f4949ea06d59480670ccfa02446fa7713d13)) + - Use default value for the routing algorithm column during business profile creation ([#2791](https://github.com/juspay/hyperswitch/pull/2791)) ([`b1fe76a`](https://github.com/juspay/hyperswitch/commit/b1fe76a82b4026d6eaa3baf4356378040880a458)) +- **routing:** Fix kgraph to exclude PM auth during construction ([#3019](https://github.com/juspay/hyperswitch/pull/3019)) ([`c6cb527`](https://github.com/juspay/hyperswitch/commit/c6cb527f07e23796c342f3562fbf3b61f1ef6801)) + +### Refactors + +- **connector:** + - [Stax] change error message from NotSupported to NotImplemented ([#2879](https://github.com/juspay/hyperswitch/pull/2879)) ([`8a4dabc`](https://github.com/juspay/hyperswitch/commit/8a4dabc61df3e6012e50f785d93808ca3349be65)) + - [Volt] change error message from NotSupported to NotImplemented ([#2878](https://github.com/juspay/hyperswitch/pull/2878)) ([`de8e31b`](https://github.com/juspay/hyperswitch/commit/de8e31b70d9b3c11e268cd1deffa71918dc4270d)) + - [Adyen] Change country and issuer type to Optional for OpenBankingUk ([#2993](https://github.com/juspay/hyperswitch/pull/2993)) ([`ab3dac7`](https://github.com/juspay/hyperswitch/commit/ab3dac79b4f138cd1f60a9afc0635dcc137a4a05)) +- **postman:** Fix payme postman collection for handling `order_details` ([#2996](https://github.com/juspay/hyperswitch/pull/2996)) ([`1e60c71`](https://github.com/juspay/hyperswitch/commit/1e60c710985b341a118bb32962bd74b406d78f69)) + +**Full Changelog:** [`v1.92.0...v1.93.0`](https://github.com/juspay/hyperswitch/compare/v1.92.0...v1.93.0) + +- - - + + ## 1.92.0 (2023-11-29) ### Features From 3fa0bdf76558ec91df8d3beef3c36658cd138b37 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:02:47 +0530 Subject: [PATCH 120/443] feat(user_role): Add APIs for user roles (#3013) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events.rs | 1 + crates/api_models/src/events/user.rs | 6 +- crates/api_models/src/events/user_role.rs | 14 ++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/user.rs | 17 ++ crates/api_models/src/user_role.rs | 82 ++++++ crates/router/src/consts.rs | 2 +- crates/router/src/consts/user_role.rs | 11 + crates/router/src/core.rs | 2 + crates/router/src/core/errors/user.rs | 22 +- crates/router/src/core/user.rs | 234 ++++++++++++++++-- crates/router/src/core/user_role.rs | 101 ++++++++ crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 13 +- crates/router/src/routes/lock_utils.rs | 10 +- crates/router/src/routes/user.rs | 66 ++++- crates/router/src/routes/user_role.rs | 84 +++++++ crates/router/src/services/authentication.rs | 3 + .../authorization/predefined_permissions.rs | 220 +++++++++++++++- crates/router/src/types/domain/user.rs | 204 ++++++++++++++- crates/router/src/utils.rs | 2 + crates/router/src/utils/user.rs | 49 ++++ crates/router/src/utils/user_role.rs | 93 +++++++ crates/router_env/src/logger/types.rs | 14 ++ 24 files changed, 1207 insertions(+), 46 deletions(-) create mode 100644 crates/api_models/src/events/user_role.rs create mode 100644 crates/api_models/src/user_role.rs create mode 100644 crates/router/src/consts/user_role.rs create mode 100644 crates/router/src/core/user_role.rs create mode 100644 crates/router/src/routes/user_role.rs create mode 100644 crates/router/src/utils/user_role.rs diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 345f827daeac..ac7cdeb83d94 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -7,6 +7,7 @@ pub mod payouts; pub mod refund; pub mod routing; pub mod user; +pub mod user_role; use common_utils::{ events::{ApiEventMetric, ApiEventsType}, diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index edfdcf1d6652..50df0c9a584b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -5,6 +5,7 @@ use crate::user::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, + CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate, }; impl ApiEventMetric for ConnectAccountResponse { @@ -23,5 +24,8 @@ common_utils::impl_misc_api_event_type!( GetMultipleMetaDataPayload, GetMetaDataResponse, GetMetaDataRequest, - SetMetaDataRequest + SetMetaDataRequest, + SwitchMerchantIdRequest, + CreateInternalUserRequest, + UserMerchantCreate ); diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs new file mode 100644 index 000000000000..aa8d13dab6df --- /dev/null +++ b/crates/api_models/src/events/user_role.rs @@ -0,0 +1,14 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::user_role::{ + AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse, + UpdateUserRoleRequest, +}; + +common_utils::impl_misc_api_event_type!( + ListRolesResponse, + RoleInfoResponse, + GetRoleRequest, + AuthorizationInfoResponse, + UpdateUserRoleRequest +); diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 8ef40d319140..056888839a54 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -26,6 +26,7 @@ pub mod refunds; pub mod routing; pub mod surcharge_decision_configs; pub mod user; +pub mod user_role; pub mod verifications; pub mod verify_connector; pub mod webhooks; diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 84659432aa6a..e0bfa50b4115 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -26,3 +26,20 @@ pub struct ChangePasswordRequest { pub new_password: Secret, pub old_password: Secret, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SwitchMerchantIdRequest { + pub merchant_id: String, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct CreateInternalUserRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UserMerchantCreate { + pub company_name: String, +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs new file mode 100644 index 000000000000..521d17e73428 --- /dev/null +++ b/crates/api_models/src/user_role.rs @@ -0,0 +1,82 @@ +#[derive(Debug, serde::Serialize)] +pub struct ListRolesResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct RoleInfoResponse { + pub role_id: &'static str, + pub permissions: Vec, + pub role_name: &'static str, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetRoleRequest { + pub role_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum Permission { + PaymentRead, + PaymentWrite, + RefundRead, + RefundWrite, + ApiKeyRead, + ApiKeyWrite, + MerchantAccountRead, + MerchantAccountWrite, + MerchantConnectorAccountRead, + MerchantConnectorAccountWrite, + ForexRead, + RoutingRead, + RoutingWrite, + DisputeRead, + DisputeWrite, + MandateRead, + MandateWrite, + FileRead, + FileWrite, + Analytics, + ThreeDsDecisionManagerWrite, + ThreeDsDecisionManagerRead, + SurchargeDecisionManagerWrite, + SurchargeDecisionManagerRead, + UsersRead, + UsersWrite, +} + +#[derive(Debug, serde::Serialize)] +pub enum PermissionModule { + Payments, + Refunds, + MerchantAccount, + Forex, + Connectors, + Routing, + Analytics, + Mandates, + Disputes, + Files, + ThreeDsDecisionManager, + SurchargeDecisionManager, +} + +#[derive(Debug, serde::Serialize)] +pub struct AuthorizationInfoResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct ModuleInfo { + pub module: PermissionModule, + pub description: &'static str, + pub permissions: Vec, +} + +#[derive(Debug, serde::Serialize)] +pub struct PermissionInfo { + pub enum_name: Permission, + pub description: &'static str, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateUserRoleRequest { + pub user_id: String, + pub role_id: String, +} diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 49c5cfacad1f..4a2d2831d103 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -1,5 +1,6 @@ #[cfg(feature = "olap")] pub mod user; +pub mod user_role; // ID generation pub(crate) const ID_LENGTH: usize = 20; @@ -64,7 +65,6 @@ pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days #[cfg(feature = "email")] pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day -pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; diff --git a/crates/router/src/consts/user_role.rs b/crates/router/src/consts/user_role.rs new file mode 100644 index 000000000000..ae1436bcd678 --- /dev/null +++ b/crates/router/src/consts/user_role.rs @@ -0,0 +1,11 @@ +// User Roles +pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only"; +pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin"; +pub const ROLE_ID_MERCHANT_ADMIN: &str = "merchant_admin"; +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; +pub const ROLE_ID_MERCHANT_VIEW_ONLY: &str = "merchant_view_only"; +pub const ROLE_ID_MERCHANT_IAM_ADMIN: &str = "merchant_iam_admin"; +pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer"; +pub const ROLE_ID_MERCHANT_OPERATOR: &str = "merchant_operator"; +pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support"; +pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 30fe1a1ce8cb..08de9cf80384 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -25,6 +25,8 @@ pub mod routing; pub mod surcharge_decision_config; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index f5c50e28ccc6..ba600917ecca 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -27,16 +27,22 @@ pub enum UserErrors { MerchantAccountCreationError(String), #[error("InvalidEmailError")] InvalidEmailError, + #[error("DuplicateOrganizationId")] + DuplicateOrganizationId, #[error("MerchantIdNotFound")] MerchantIdNotFound, #[error("MetadataAlreadySet")] MetadataAlreadySet, - #[error("DuplicateOrganizationId")] - DuplicateOrganizationId, + #[error("InvalidRoleId")] + InvalidRoleId, + #[error("InvalidRoleOperation")] + InvalidRoleOperation, #[error("IpAddressParsingFailed")] IpAddressParsingFailed, #[error("InvalidMetadataRequest")] InvalidMetadataRequest, + #[error("MerchantIdParsingError")] + MerchantIdParsingError, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -95,6 +101,15 @@ impl common_utils::errors::ErrorSwitch { + AER::BadRequest(ApiError::new(sub_code, 22, "Invalid Role ID", None)) + } + Self::InvalidRoleOperation => AER::BadRequest(ApiError::new( + sub_code, + 23, + "User Role Operation Not Supported", + None, + )), Self::IpAddressParsingFailed => { AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) } @@ -104,6 +119,9 @@ impl common_utils::errors::ErrorSwitch { + AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None)) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 9a199d09b8fd..8e7f6c27a7da 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,5 +1,5 @@ -use api_models::user as api; -use diesel_models::enums::UserStatus; +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user as storage_user}; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use router_env::env; @@ -9,16 +9,17 @@ use crate::{ consts, db::user::UserInterface, routes::AppState, - services::{authentication::UserFromToken, ApplicationResponse}, + services::{authentication as auth, ApplicationResponse}, types::domain, + utils, }; pub mod dashboard_metadata; pub async fn connect_account( state: AppState, - request: api::ConnectAccountRequest, -) -> UserResponse { + request: user_api::ConnectAccountRequest, +) -> UserResponse { let find_user = state .store .find_user_by_email(request.email.clone().expose().expose().as_str()) @@ -34,15 +35,17 @@ pub async fn connect_account( .get_jwt_auth_token(state.clone(), user_role.org_id) .await?; - return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, - user_id: user_from_db.get_user_id().to_string(), - })); + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + }, + )); } else if find_user .map_err(|e| e.current_context().is_db_not_found()) .err() @@ -64,7 +67,7 @@ pub async fn connect_account( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -94,15 +97,17 @@ pub async fn connect_account( logger::info!(?send_email_result); } - return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, - user_id: user_from_db.get_user_id().to_string(), - })); + return Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + }, + )); } else { Err(UserErrors::InternalServerError.into()) } @@ -110,8 +115,8 @@ pub async fn connect_account( pub async fn change_password( state: AppState, - request: api::ChangePasswordRequest, - user_from_token: UserFromToken, + request: user_api::ChangePasswordRequest, + user_from_token: auth::UserFromToken, ) -> UserResponse<()> { let user: domain::UserFromStorage = UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id) @@ -139,3 +144,180 @@ pub async fn change_password( Ok(ApplicationResponse::StatusOk) } + +pub async fn create_internal_user( + state: AppState, + request: user_api::CreateInternalUserRequest, +) -> UserResponse<()> { + let new_user = domain::NewUser::try_from(request)?; + + let mut store_user: storage_user::UserNew = new_user.clone().try_into()?; + store_user.set_is_verified(true); + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + consts::user_role::INTERNAL_USER_MERCHANT_ID, + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + state + .store + .find_merchant_account_by_merchant_id( + consts::user_role::INTERNAL_USER_MERCHANT_ID, + &key_store, + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + state + .store + .insert_user(store_user) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .map(domain::user::UserFromStorage::from)?; + + new_user + .insert_user_role_in_db( + state, + consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), + UserStatus::Active, + ) + .await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn switch_merchant_id( + state: AppState, + request: user_api::SwitchMerchantIdRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse { + if !utils::user_role::is_internal_role(&user_from_token.role_id) { + let merchant_list = + utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) + .await?; + if !merchant_list.contains(&request.merchant_id) { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User doesn't have access to switch"); + } + } + + if user_from_token.merchant_id == request.merchant_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User switch to same merchant id."); + } + + let user = state + .store + .find_user_by_id(&user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + request.merchant_id.as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + let org_id = state + .store + .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .organization_id; + + let user = domain::UserFromStorage::from(user); + let user_role = state + .store + .find_user_role_by_user_id(user.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)?; + + let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id( + state.clone(), + request.merchant_id.clone(), + org_id, + )) + .await? + .into(); + + Ok(ApplicationResponse::Json( + user_api::ConnectAccountResponse { + merchant_id: request.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: user_role.role_id, + }, + )) +} + +pub async fn create_merchant_account( + state: AppState, + user_from_token: auth::UserFromToken, + req: user_api::UserMerchantCreate, +) -> UserResponse<()> { + let user_from_db: domain::UserFromStorage = + user_from_token.get_user(state.clone()).await?.into(); + + let new_user = domain::NewUser::try_from((user_from_db, req, user_from_token))?; + let new_merchant = new_user.get_new_merchant(); + new_merchant + .create_new_merchant_and_insert_in_db(state.to_owned()) + .await?; + + let role_insertion_res = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await; + if let Err(e) = role_insertion_res { + let _ = state + .store + .delete_merchant_account_by_merchant_id(new_merchant.get_merchant_id().as_str()) + .await; + return Err(e); + } + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs new file mode 100644 index 000000000000..2b7752d1904b --- /dev/null +++ b/crates/router/src/core/user_role.rs @@ -0,0 +1,101 @@ +use api_models::user_role as user_role_api; +use diesel_models::user_role::UserRoleUpdate; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse}, + routes::AppState, + services::{ + authentication::{self as auth}, + authorization::{info, predefined_permissions}, + ApplicationResponse, + }, + utils, +}; + +pub async fn get_authorization_info( + _state: AppState, +) -> UserResponse { + Ok(ApplicationResponse::Json( + user_role_api::AuthorizationInfoResponse( + info::get_authorization_info() + .into_iter() + .filter_map(|module| module.try_into().ok()) + .collect(), + ), + )) +} + +pub async fn list_roles(_state: AppState) -> UserResponse { + Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse( + predefined_permissions::PREDEFINED_PERMISSIONS + .iter() + .filter_map(|(role_id, role_info)| { + utils::user_role::get_role_name_and_permission_response(role_info).map( + |(permissions, role_name)| user_role_api::RoleInfoResponse { + permissions, + role_id, + role_name, + }, + ) + }) + .collect(), + ))) +} + +pub async fn get_role( + _state: AppState, + role: user_role_api::GetRoleRequest, +) -> UserResponse { + let info = predefined_permissions::PREDEFINED_PERMISSIONS + .get_key_value(role.role_id.as_str()) + .and_then(|(role_id, role_info)| { + utils::user_role::get_role_name_and_permission_response(role_info).map( + |(permissions, role_name)| user_role_api::RoleInfoResponse { + permissions, + role_id, + role_name, + }, + ) + }) + .ok_or(UserErrors::InvalidRoleId)?; + + Ok(ApplicationResponse::Json(info)) +} + +pub async fn update_user_role( + state: AppState, + user_from_token: auth::UserFromToken, + req: user_role_api::UpdateUserRoleRequest, +) -> UserResponse<()> { + let merchant_id = user_from_token.merchant_id; + let role_id = req.role_id.clone(); + utils::user_role::validate_role_id(role_id.as_str())?; + + if user_from_token.user_id == req.user_id { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("Admin User Changing their role"); + } + + state + .store + .update_user_role_by_user_id_merchant_id( + req.user_id.as_str(), + merchant_id.as_str(), + UserRoleUpdate::UpdateRole { + role_id, + modified_by: user_from_token.user_id, + }, + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + return e + .change_context(UserErrors::InvalidRoleOperation) + .attach_printable("UserId MerchantId not found"); + } + e.change_context(UserErrors::InternalServerError) + })?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 22c2610d3255..b19ef5d7016b 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -27,6 +27,8 @@ pub mod refunds; pub mod routing; #[cfg(feature = "olap")] pub mod user; +#[cfg(feature = "olap")] +pub mod user_role; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2f8932057fb4..5f0c89ed6b4c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -23,7 +23,7 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve #[cfg(feature = "olap")] use super::{ admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, - user::*, + user::*, user_role::*, }; use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -812,6 +812,17 @@ impl User { .route(web::post().to(set_merchant_scoped_dashboard_metadata)), ) .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) + .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) + .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) + .service( + web::resource("/create_merchant") + .route(web::post().to(user_merchant_account_create)), + ) + // User Role APIs + .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) + .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) + .service(web::resource("/role/list").route(web::get().to(list_roles))) + .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 72bc3c9cd417..552deb85a2e1 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -27,6 +27,7 @@ pub enum ApiIdentifier { RustLockerMigration, Gsm, User, + UserRole, } impl From for ApiIdentifier { @@ -151,7 +152,14 @@ impl From for ApiIdentifier { | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata - | Flow::VerifyPaymentConnector => Self::User, + | Flow::VerifyPaymentConnector + | Flow::InternalUserSignup + | Flow::SwitchMerchant + | Flow::UserMerchantAccountCreate => Self::User, + + Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { + Self::UserRole + } } } } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 3f5f7815ffbc..89c4bd4c90ec 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -5,7 +5,7 @@ use router_env::Flow; use super::AppState; use crate::{ - core::{api_locking, user}, + core::{api_locking, user as user_core}, services::{ api, authentication::{self as auth}, @@ -26,7 +26,7 @@ pub async fn user_connect_account( state, &http_req, req_payload.clone(), - |state, _, req_body| user::connect_account(state, req_body), + |state, _, req_body| user_core::connect_account(state, req_body), &auth::NoAuth, api_locking::LockAction::NotApplicable, )) @@ -44,7 +44,7 @@ pub async fn change_password( state.clone(), &http_req, json_payload.into_inner(), - |state, user, req| user::change_password(state, req, user), + |state, user, req| user_core::change_password(state, req, user), &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, )) @@ -70,7 +70,7 @@ pub async fn set_merchant_scoped_dashboard_metadata( state, &req, payload, - user::dashboard_metadata::set_metadata, + user_core::dashboard_metadata::set_metadata, &auth::JWTAuth(Permission::MerchantAccountWrite), api_locking::LockAction::NotApplicable, )) @@ -96,9 +96,65 @@ pub async fn get_multiple_dashboard_metadata( state, &req, payload, - user::dashboard_metadata::get_multiple_metadata, + user_core::dashboard_metadata::get_multiple_metadata, &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, )) .await } + +pub async fn internal_user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::InternalUserSignup; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _, req| user_core::create_internal_user(state, req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn switch_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SwitchMerchant; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, user, req| user_core::switch_merchant_id(state, req, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_merchant_account_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserMerchantAccountCreate; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::UserFromToken, json_payload| { + user_core::create_merchant_account(state, auth, json_payload) + }, + &auth::JWTAuth(Permission::MerchantAccountCreate), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs new file mode 100644 index 000000000000..c96e099ab163 --- /dev/null +++ b/crates/router/src/routes/user_role.rs @@ -0,0 +1,84 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::user_role as user_role_api; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, user_role as user_role_core}, + services::{ + api, + authentication::{self as auth}, + authorization::permissions::Permission, + }, +}; + +pub async fn get_authorization_info( + state: web::Data, + http_req: HttpRequest, +) -> HttpResponse { + let flow = Flow::GetAuthorizationInfo; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, _: (), _| user_role_core::get_authorization_info(state), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn list_roles(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::ListRoles; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, _: (), _| user_role_core::list_roles(state), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_role( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::GetRole; + let request_payload = user_role_api::GetRoleRequest { + role_id: path.into_inner(), + }; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + request_payload, + |state, _: (), req| user_role_core::get_role(state, req), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn update_user_role( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UpdateUserRole; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + user_role_core::update_user_role, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index b01e3762bfab..8a0cd7c729e9 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -444,6 +444,9 @@ where ) -> RouterResult<(UserFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + Ok(( UserFromToken { user_id: payload.user_id.clone(), diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs index 89fa2c8f739c..a9f2b864d0ad 100644 --- a/crates/router/src/services/authorization/predefined_permissions.rs +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -28,7 +28,67 @@ impl RoleInfo { pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy::new(|| { let mut roles = HashMap::new(); roles.insert( - consts::ROLE_ID_ORGANIZATION_ADMIN, + consts::user_role::ROLE_ID_INTERNAL_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + Permission::MerchantAccountCreate, + ], + name: None, + is_invitable: false, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ForexRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::Analytics, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::UsersRead, + ], + name: None, + is_invitable: false, + }, + ); + + roles.insert( + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN, RoleInfo { permissions: vec![ Permission::PaymentRead, @@ -63,6 +123,164 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: is_invitable: false, }, ); + + // MERCHANT ROLES + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MandateRead, + Permission::MandateWrite, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("Admin"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("View Only"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + Permission::UsersWrite, + ], + name: Some("IAM"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_DEVELOPER, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::ApiKeyRead, + Permission::ApiKeyWrite, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::RoutingRead, + Permission::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerRead, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Developer"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_OPERATOR, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::PaymentWrite, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ApiKeyRead, + Permission::MerchantAccountRead, + Permission::ForexRead, + Permission::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite, + Permission::RoutingRead, + Permission::RoutingWrite, + Permission::ThreeDsDecisionManagerRead, + Permission::ThreeDsDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite, + Permission::DisputeRead, + Permission::MandateRead, + Permission::FileRead, + Permission::Analytics, + Permission::UsersRead, + ], + name: Some("Operator"), + is_invitable: true, + }, + ); + roles.insert( + consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT, + RoleInfo { + permissions: vec![ + Permission::PaymentRead, + Permission::RefundRead, + Permission::RefundWrite, + Permission::ForexRead, + Permission::DisputeRead, + Permission::DisputeWrite, + Permission::MerchantAccountRead, + Permission::MerchantConnectorAccountRead, + Permission::MandateRead, + Permission::FileRead, + Permission::FileWrite, + Permission::Analytics, + ], + name: Some("Customer Support"), + is_invitable: true, + }, + ); roles }); diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 7e723bf00c32..0c7760f84d36 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -1,6 +1,8 @@ use std::{collections::HashSet, ops, str::FromStr}; -use api_models::{admin as admin_api, organization as api_org, user as user_api}; +use api_models::{ + admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api, +}; use common_utils::pii; use diesel_models::{ enums::UserStatus, @@ -12,17 +14,21 @@ use diesel_models::{ use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; use once_cell::sync::Lazy; +use router_env::env; use unicode_segmentation::UnicodeSegmentation; use crate::{ - consts::user as consts, + consts, core::{ admin, errors::{UserErrors, UserResult}, }, db::StorageInterface, routes::AppState, - services::authentication::AuthToken, + services::{ + authentication::{AuthToken, UserFromToken}, + authorization::info, + }, types::transformers::ForeignFrom, utils::user::password, }; @@ -36,7 +42,7 @@ impl UserName { pub fn new(name: Secret) -> UserResult { let name = name.expose(); let is_empty_or_whitespace = name.trim().is_empty(); - let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH; + let is_too_long = name.graphemes(true).count() > consts::user::MAX_NAME_LENGTH; let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g)); @@ -167,7 +173,8 @@ impl UserCompanyName { pub fn new(company_name: String) -> UserResult { let company_name = company_name.trim(); let is_empty_or_whitespace = company_name.is_empty(); - let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH; + let is_too_long = + company_name.graphemes(true).count() > consts::user::MAX_COMPANY_NAME_LENGTH; let is_all_valid_characters = company_name .chars() @@ -216,9 +223,47 @@ impl From for NewUserOrganization { } } +impl From for NewUserOrganization { + fn from(_value: user_api::CreateInternalUserRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +impl From for NewUserOrganization { + fn from(value: UserMerchantCreateRequestWithToken) -> Self { + Self(diesel_org::OrganizationNew { + org_id: value.2.org_id, + org_name: Some(value.1.company_name), + }) + } +} + +#[derive(Clone)] +pub struct MerchantId(String); + +impl MerchantId { + pub fn new(merchant_id: String) -> UserResult { + let merchant_id = merchant_id.trim().to_lowercase().replace(' ', "_"); + let is_empty_or_whitespace = merchant_id.is_empty(); + + let is_all_valid_characters = merchant_id.chars().all(|x| x.is_alphanumeric() || x == '_'); + if is_empty_or_whitespace || !is_all_valid_characters { + Err(UserErrors::MerchantIdParsingError.into()) + } else { + Ok(Self(merchant_id.to_string())) + } + } + + pub fn get_secret(&self) -> String { + self.0.clone() + } +} + #[derive(Clone)] pub struct NewUserMerchant { - merchant_id: String, + merchant_id: MerchantId, company_name: Option, new_organization: NewUserOrganization, } @@ -229,7 +274,7 @@ impl NewUserMerchant { } pub fn get_merchant_id(&self) -> String { - self.merchant_id.clone() + self.merchant_id.get_secret() } pub fn get_new_organization(&self) -> NewUserOrganization { @@ -293,7 +338,10 @@ impl TryFrom for NewUserMerchant { type Error = error_stack::Report; fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { - let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp()); + let merchant_id = MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))?; let new_organization = NewUserOrganization::from(value); Ok(Self { @@ -304,6 +352,45 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult { + let merchant_id = + MerchantId::new(consts::user_role::INTERNAL_USER_MERCHANT_ID.to_string())?; + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + +type UserMerchantCreateRequestWithToken = + (UserFromStorage, user_api::UserMerchantCreate, UserFromToken); + +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: UserMerchantCreateRequestWithToken) -> UserResult { + let merchant_id = if matches!(env::which(), env::Env::Production) { + MerchantId::new(value.1.company_name.clone())? + } else { + MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))? + }; + Ok(Self { + merchant_id, + company_name: Some(UserCompanyName::new(value.1.company_name.clone())?), + new_organization: NewUserOrganization::from(value), + }) + } +} + #[derive(Clone)] pub struct NewUser { user_id: String, @@ -428,6 +515,44 @@ impl TryFrom for NewUser { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::CreateInternalUserRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: UserMerchantCreateRequestWithToken) -> Result { + let user = value.0.clone(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id: user.0.user_id, + name: UserName::new(user.0.name)?, + email: user.0.email.clone().try_into()?, + password: UserPassword::new(user.0.password)?, + new_merchant, + }) + } +} + +#[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); impl From for UserFromStorage { @@ -475,6 +600,23 @@ impl UserFromStorage { .await } + pub async fn get_jwt_auth_token_with_custom_merchant_id( + &self, + state: AppState, + merchant_id: String, + org_id: String, + ) -> UserResult { + let role_id = self.get_role_from_db(state.clone()).await?.role_id; + AuthToken::new_token( + self.0.user_id.clone(), + merchant_id, + role_id, + &state.conf, + org_id, + ) + .await + } + pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store @@ -483,3 +625,49 @@ impl UserFromStorage { .change_context(UserErrors::InternalServerError) } } + +impl TryFrom for user_role_api::ModuleInfo { + type Error = (); + fn try_from(value: info::ModuleInfo) -> Result { + let mut permissions = Vec::with_capacity(value.permissions.len()); + for permission in value.permissions { + let permission = permission.try_into()?; + permissions.push(permission); + } + Ok(Self { + module: value.module.into(), + description: value.description, + permissions, + }) + } +} + +impl From for user_role_api::PermissionModule { + fn from(value: info::PermissionModule) -> Self { + match value { + info::PermissionModule::Payments => Self::Payments, + info::PermissionModule::Refunds => Self::Refunds, + info::PermissionModule::MerchantAccount => Self::MerchantAccount, + info::PermissionModule::Forex => Self::Forex, + info::PermissionModule::Connectors => Self::Connectors, + info::PermissionModule::Routing => Self::Routing, + info::PermissionModule::Analytics => Self::Analytics, + info::PermissionModule::Mandates => Self::Mandates, + info::PermissionModule::Disputes => Self::Disputes, + info::PermissionModule::Files => Self::Files, + info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager, + info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager, + } + } +} + +impl TryFrom for user_role_api::PermissionInfo { + type Error = (); + fn try_from(value: info::PermissionInfo) -> Result { + let enum_name = (&value.enum_name).try_into()?; + Ok(Self { + enum_name, + description: value.description, + }) + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 81968cd9b628..f1590342e17c 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -7,6 +7,8 @@ pub mod storage_partitioning; #[cfg(feature = "olap")] pub mod user; #[cfg(feature = "olap")] +pub mod user_role; +#[cfg(feature = "olap")] pub mod verify_connector; use std::fmt::Debug; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 824f7f63af75..4dc54ba3f708 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,2 +1,51 @@ +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authentication::UserFromToken, + types::domain::MerchantAccount, +}; + pub mod dashboard_metadata; pub mod password; + +impl UserFromToken { + pub async fn get_merchant_account(&self, state: AppState) -> UserResult { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &self.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + let merchant_account = state + .store + .find_merchant_account_by_merchant_id(&self.merchant_id, &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + Ok(merchant_account) + } + + pub async fn get_user(&self, state: AppState) -> UserResult { + let user = state + .store + .find_user_by_id(&self.user_id) + .await + .change_context(UserErrors::InternalServerError)?; + Ok(user) + } +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs new file mode 100644 index 000000000000..0026984fdb9a --- /dev/null +++ b/crates/router/src/utils/user_role.rs @@ -0,0 +1,93 @@ +use api_models::user_role as user_role_api; +use diesel_models::enums::UserStatus; +use error_stack::ResultExt; +use router_env::logger; + +use crate::{ + consts, + core::errors::{UserErrors, UserResult}, + routes::AppState, + services::authorization::{ + permissions::Permission, + predefined_permissions::{self, RoleInfo}, + }, +}; + +pub fn is_internal_role(role_id: &str) -> bool { + role_id == consts::user_role::ROLE_ID_INTERNAL_ADMIN + || role_id == consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER +} + +pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult> { + Ok(state + .store + .list_user_roles_by_user_id(user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|ele| { + if ele.status == UserStatus::Active { + return Some(ele.merchant_id); + } + None + }) + .collect()) +} + +pub fn validate_role_id(role_id: &str) -> UserResult<()> { + if predefined_permissions::is_role_invitable(role_id) { + return Ok(()); + } + Err(UserErrors::InvalidRoleId.into()) +} + +pub fn get_role_name_and_permission_response( + role_info: &RoleInfo, +) -> Option<(Vec, &'static str)> { + role_info + .get_permissions() + .iter() + .map(TryInto::try_into) + .collect::, _>>() + .ok() + .zip(role_info.get_name()) +} + +impl TryFrom<&Permission> for user_role_api::Permission { + type Error = (); + fn try_from(value: &Permission) -> Result { + match value { + Permission::PaymentRead => Ok(Self::PaymentRead), + Permission::PaymentWrite => Ok(Self::PaymentWrite), + Permission::RefundRead => Ok(Self::RefundRead), + Permission::RefundWrite => Ok(Self::RefundWrite), + Permission::ApiKeyRead => Ok(Self::ApiKeyRead), + Permission::ApiKeyWrite => Ok(Self::ApiKeyWrite), + Permission::MerchantAccountRead => Ok(Self::MerchantAccountRead), + Permission::MerchantAccountWrite => Ok(Self::MerchantAccountWrite), + Permission::MerchantConnectorAccountRead => Ok(Self::MerchantConnectorAccountRead), + Permission::MerchantConnectorAccountWrite => Ok(Self::MerchantConnectorAccountWrite), + Permission::ForexRead => Ok(Self::ForexRead), + Permission::RoutingRead => Ok(Self::RoutingRead), + Permission::RoutingWrite => Ok(Self::RoutingWrite), + Permission::DisputeRead => Ok(Self::DisputeRead), + Permission::DisputeWrite => Ok(Self::DisputeWrite), + Permission::MandateRead => Ok(Self::MandateRead), + Permission::MandateWrite => Ok(Self::MandateWrite), + Permission::FileRead => Ok(Self::FileRead), + Permission::FileWrite => Ok(Self::FileWrite), + Permission::Analytics => Ok(Self::Analytics), + Permission::ThreeDsDecisionManagerWrite => Ok(Self::ThreeDsDecisionManagerWrite), + Permission::ThreeDsDecisionManagerRead => Ok(Self::ThreeDsDecisionManagerRead), + Permission::SurchargeDecisionManagerWrite => Ok(Self::SurchargeDecisionManagerWrite), + Permission::SurchargeDecisionManagerRead => Ok(Self::SurchargeDecisionManagerRead), + Permission::UsersRead => Ok(Self::UsersRead), + Permission::UsersWrite => Ok(Self::UsersWrite), + + Permission::MerchantAccountCreate => { + logger::error!("Invalid use of internal permission"); + Err(()) + } + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7b87d2703640..eefdc86affad 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -265,6 +265,20 @@ pub enum Flow { GetMutltipleDashboardMetadata, /// Payment Connector Verify VerifyPaymentConnector, + /// Internal user signup + InternalUserSignup, + /// Switch merchant + SwitchMerchant, + /// Get permission info + GetAuthorizationInfo, + /// List roles + ListRoles, + /// Get role + GetRole, + /// Update user role + UpdateUserRole, + /// Create merchant account for user in a org + UserMerchantAccountCreate, } /// From 668b943403df2b3bb354dd093b8ec073a2618bda Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Thu, 30 Nov 2023 10:53:05 -0800 Subject: [PATCH 121/443] refactor(connector): [Multisafe Pay] change error message from not supported to not implemented (#2851) --- crates/router/src/connector/multisafepay/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 7672566f8274..0a034724a629 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -262,10 +262,9 @@ impl TryFrom for Gateway { utils::CardIssuer::Visa => Ok(Self::Visa), utils::CardIssuer::DinersClub | utils::CardIssuer::JCB - | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Multisafe pay", - } + | utils::CardIssuer::CarteBlanche => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Multisafe pay"), + ) .into()), } } From bc79d522c30aa036378cf1e01354c422585cc226 Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Thu, 30 Nov 2023 21:53:37 +0300 Subject: [PATCH 122/443] refactor(connector): [Shift4] change error message from NotSupported to NotImplemented (#2880) --- .../src/connector/shift4/transformers.rs | 89 +++++++------------ 1 file changed, 31 insertions(+), 58 deletions(-) diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 606da2129fb0..ce68aad25c50 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -168,10 +168,9 @@ impl TryFrom<&types::RouterData { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()) } } @@ -184,13 +183,8 @@ impl TryFrom<&api_models::payments::WalletData> for Shift4PaymentMethod { match wallet_data { payments::WalletData::AliPayRedirect(_) | payments::WalletData::ApplePay(_) - | payments::WalletData::WeChatPayRedirect(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::WalletData::AliPayQr(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::AliPayQr(_) | payments::WalletData::AliPayHkRedirect(_) | payments::WalletData::MomoRedirect(_) | payments::WalletData::KakaoPayRedirect(_) @@ -212,10 +206,9 @@ impl TryFrom<&api_models::payments::WalletData> for Shift4PaymentMethod { | payments::WalletData::TouchNGoRedirect(_) | payments::WalletData::WeChatPayQr(_) | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -227,13 +220,8 @@ impl TryFrom<&api_models::payments::BankTransferData> for Shift4PaymentMethod { bank_transfer_data: &api_models::payments::BankTransferData, ) -> Result { match bank_transfer_data { - payments::BankTransferData::MultibancoBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::BankTransferData::AchBankTransfer { .. } + payments::BankTransferData::MultibancoBankTransfer { .. } + | payments::BankTransferData::AchBankTransfer { .. } | payments::BankTransferData::SepaBankTransfer { .. } | payments::BankTransferData::BacsBankTransfer { .. } | payments::BankTransferData::PermataBankTransfer { .. } @@ -244,10 +232,9 @@ impl TryFrom<&api_models::payments::BankTransferData> for Shift4PaymentMethod { | payments::BankTransferData::DanamonVaBankTransfer { .. } | payments::BankTransferData::MandiriVaBankTransfer { .. } | payments::BankTransferData::Pix {} - | payments::BankTransferData::Pse {} => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::BankTransferData::Pse {} => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -257,11 +244,8 @@ impl TryFrom<&api_models::payments::VoucherData> for Shift4PaymentMethod { type Error = Error; fn try_from(voucher_data: &api_models::payments::VoucherData) -> Result { match voucher_data { - payments::VoucherData::Boleto(_) => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()), - payments::VoucherData::Efecty + payments::VoucherData::Boleto(_) + | payments::VoucherData::Efecty | payments::VoucherData::PagoEfectivo | payments::VoucherData::RedCompra | payments::VoucherData::RedPagos @@ -273,10 +257,9 @@ impl TryFrom<&api_models::payments::VoucherData> for Shift4PaymentMethod { | payments::VoucherData::MiniStop(_) | payments::VoucherData::FamilyMart(_) | payments::VoucherData::Seicomart(_) - | payments::VoucherData::PayEasy(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + | payments::VoucherData::PayEasy(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -286,15 +269,12 @@ impl TryFrom<&api_models::payments::GiftCardData> for Shift4PaymentMethod { type Error = Error; fn try_from(gift_card_data: &api_models::payments::GiftCardData) -> Result { match gift_card_data { - payments::GiftCardData::Givex(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + payments::GiftCardData::Givex(_) | payments::GiftCardData::PaySafeCard {} => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) + .into()) } - .into()), - payments::GiftCardData::PaySafeCard {} => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()), } } } @@ -401,10 +381,9 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: "Flow".to_string(), - connector: "Shift4", - } + | None => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()), } } @@ -421,13 +400,8 @@ impl TryFrom<&payments::BankRedirectData> for PaymentMethodType { payments::BankRedirectData::BancontactCard { .. } | payments::BankRedirectData::Blik { .. } | payments::BankRedirectData::Trustly { .. } - | payments::BankRedirectData::Przelewy24 { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Shift4"), - ) - .into()) - } - payments::BankRedirectData::Bizum {} + | payments::BankRedirectData::Przelewy24 { .. } + | payments::BankRedirectData::Bizum {} | payments::BankRedirectData::Interac { .. } | payments::BankRedirectData::OnlineBankingCzechRepublic { .. } | payments::BankRedirectData::OnlineBankingFinland { .. } @@ -436,10 +410,9 @@ impl TryFrom<&payments::BankRedirectData> for PaymentMethodType { | payments::BankRedirectData::OpenBankingUk { .. } | payments::BankRedirectData::OnlineBankingFpx { .. } | payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Shift4"), + ) .into()) } } From b5934674e518f991a8a575ad01b971dd086eeb40 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Fri, 1 Dec 2023 13:18:13 +0530 Subject: [PATCH 123/443] fix(config): add kms decryption support for sqlx password (#3029) --- crates/router/src/routes/app.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5f0c89ed6b4c..d462f4a27390 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1,10 +1,14 @@ use std::sync::Arc; use actix_web::{web, Scope}; +#[cfg(all(feature = "kms", feature = "olap"))] +use analytics::AnalyticsConfig; #[cfg(feature = "email")] use external_services::email::{ses::AwsSes, EmailService}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; +#[cfg(all(feature = "olap", feature = "kms"))] +use masking::PeekInterface; use router_env::tracing_actix_web::RequestId; use scheduler::SchedulerInterface; use storage_impl::MockDb; @@ -123,7 +127,8 @@ impl AppState { /// /// Panics if Store can't be created or JWE decryption fails pub async fn with_storage( - conf: settings::Settings, + #[cfg_attr(not(all(feature = "olap", feature = "kms")), allow(unused_mut))] + mut conf: settings::Settings, storage_impl: StorageImpl, shut_down_signal: oneshot::Sender<()>, api_client: Box, @@ -165,6 +170,21 @@ impl AppState { ), }; + #[cfg(all(feature = "kms", feature = "olap"))] + #[allow(clippy::expect_used)] + match conf.analytics { + AnalyticsConfig::Clickhouse { .. } => {} + AnalyticsConfig::Sqlx { ref mut sqlx } + | AnalyticsConfig::CombinedCkh { ref mut sqlx, .. } + | AnalyticsConfig::CombinedSqlx { ref mut sqlx, .. } => { + sqlx.password = kms_client + .decrypt(&sqlx.password.peek()) + .await + .expect("Failed to decrypt password") + .into(); + } + }; + #[cfg(feature = "olap")] let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; From 5edabae701d5c4901dc8c208b043390d828ff2a3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:02:05 +0000 Subject: [PATCH 124/443] chore(version): v1.94.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3831e3d1caf3..3bbdac921fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.94.0 (2023-12-01) + +### Features + +- **user_role:** Add APIs for user roles ([#3013](https://github.com/juspay/hyperswitch/pull/3013)) ([`3fa0bdf`](https://github.com/juspay/hyperswitch/commit/3fa0bdf76558ec91df8d3beef3c36658cd138b37)) + +### Bug Fixes + +- **config:** Add kms decryption support for sqlx password ([#3029](https://github.com/juspay/hyperswitch/pull/3029)) ([`b593467`](https://github.com/juspay/hyperswitch/commit/b5934674e518f991a8a575ad01b971dd086eeb40)) + +### Refactors + +- **connector:** + - [Multisafe Pay] change error message from not supported to not implemented ([#2851](https://github.com/juspay/hyperswitch/pull/2851)) ([`668b943`](https://github.com/juspay/hyperswitch/commit/668b943403df2b3bb354dd093b8ec073a2618bda)) + - [Shift4] change error message from NotSupported to NotImplemented ([#2880](https://github.com/juspay/hyperswitch/pull/2880)) ([`bc79d52`](https://github.com/juspay/hyperswitch/commit/bc79d522c30aa036378cf1e01354c422585cc226)) + +**Full Changelog:** [`v1.93.0...v1.94.0`](https://github.com/juspay/hyperswitch/compare/v1.93.0...v1.94.0) + +- - - + + ## 1.93.0 (2023-11-30) ### Features From 2ac5b2cd764c0aad53ac7c672dfcc9132fa5668f Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:29:14 +0530 Subject: [PATCH 125/443] fix(wasm): fix wasm function to return the categories for keys with their description respectively (#3023) --- crates/euclid_wasm/src/lib.rs | 27 ++++++++++++++++++++------- crates/euclid_wasm/src/types.rs | 3 ++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index cab82f8ce411..78c7677fe75c 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -254,12 +254,25 @@ pub fn add_two(n1: i64, n2: i64) -> i64 { } #[wasm_bindgen(js_name = getDescriptionCategory)] -pub fn get_description_category(key: &str) -> JsResult { - let key = dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; +pub fn get_description_category() -> JsResult { + let keys = dir::DirKeyKind::VARIANTS + .iter() + .copied() + .filter(|s| s != &"Connector") + .collect::>(); + let mut category: HashMap, Vec>> = HashMap::new(); + for key in keys { + let dir_key = + dir::DirKeyKind::from_str(key).map_err(|_| "Invalid key received".to_string())?; + let details = types::Details { + description: dir_key.get_detailed_message(), + kind: dir_key.clone(), + }; + category + .entry(dir_key.get_str("Category")) + .and_modify(|val| val.push(details.clone())) + .or_insert(vec![details]); + } - let result = types::Details { - description: key.get_detailed_message(), - category: key.get_str("Category"), - }; - Ok(serde_wasm_bindgen::to_value(&result)?) + Ok(serde_wasm_bindgen::to_value(&category)?) } diff --git a/crates/euclid_wasm/src/types.rs b/crates/euclid_wasm/src/types.rs index ea40449971bc..6353d9009c36 100644 --- a/crates/euclid_wasm/src/types.rs +++ b/crates/euclid_wasm/src/types.rs @@ -1,7 +1,8 @@ +use euclid::frontend::dir::DirKeyKind; use serde::Serialize; #[derive(Serialize, Clone)] pub struct Details<'a> { pub description: Option<&'a str>, - pub category: Option<&'a str>, + pub kind: DirKeyKind, } From d883cd18972c5f9e8350e9a3f4e5cd56ec2c0787 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:26:41 +0530 Subject: [PATCH 126/443] fix(connector): [Paypal] Parse response for Cards with no 3DS check (#3021) --- crates/router/src/connector/paypal.rs | 157 ++++++++++-------- .../src/connector/paypal/transformers.rs | 14 +- 2 files changed, 102 insertions(+), 69 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index c60b20bb367d..a0d391789020 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -570,42 +570,95 @@ impl .parse_struct("paypal PaypalPreProcessingResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - // permutation for status to continue payment - match ( - response - .payment_source - .card - .authentication_result - .three_d_secure - .enrollment_status - .as_ref(), - response - .payment_source - .card - .authentication_result - .three_d_secure - .authentication_status - .as_ref(), - response - .payment_source - .card - .authentication_result - .liability_shift - .clone(), - ) { - ( - Some(paypal::EnrollementStatus::Ready), - Some(paypal::AuthenticationStatus::Success), - paypal::LiabilityShift::Possible, - ) - | ( - Some(paypal::EnrollementStatus::Ready), - Some(paypal::AuthenticationStatus::Attempted), - paypal::LiabilityShift::Possible, - ) - | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) - | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) - | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + match response { + // if card supports 3DS check for liability + paypal::PaypalPreProcessingResponse::PaypalLiabilityResponse(liability_response) => { + // permutation for status to continue payment + match ( + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + liability_response + .payment_source + .card + .authentication_result + .liability_shift + .clone(), + ) { + ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Success), + paypal::LiabilityShift::Possible, + ) + | ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Attempted), + paypal::LiabilityShift::Possible, + ) + | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + Ok(types::PaymentsPreProcessingRouterData { + status: storage_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..data.clone() + }) + } + _ => Ok(types::PaymentsPreProcessingRouterData { + response: Err(ErrorResponse { + attempt_status: Some(enums::AttemptStatus::Failure), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + connector_transaction_id: None, + reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", + consts::CANNOT_CONTINUE_AUTH, + liability_response + .payment_source + .card + .authentication_result + .liability_shift, + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + liability_response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + // if card does not supports 3DS check for liability + paypal::PaypalPreProcessingResponse::PaypalNonLiablityResponse(_) => { Ok(types::PaymentsPreProcessingRouterData { status: storage_enums::AttemptStatus::AuthenticationSuccessful, response: Ok(types::PaymentsResponseData::TransactionResponse { @@ -620,38 +673,6 @@ impl ..data.clone() }) } - _ => Ok(types::PaymentsPreProcessingRouterData { - response: Err(ErrorResponse { - attempt_status: Some(enums::AttemptStatus::Failure), - code: consts::NO_ERROR_CODE.to_string(), - message: consts::NO_ERROR_MESSAGE.to_string(), - connector_transaction_id: None, - reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", - consts::CANNOT_CONTINUE_AUTH, - response - .payment_source - .card - .authentication_result - .liability_shift, - response - .payment_source - .card - .authentication_result - .three_d_secure - .enrollment_status - .unwrap_or(paypal::EnrollementStatus::Null), - response - .payment_source - .card - .authentication_result - .three_d_secure - .authentication_status - .unwrap_or(paypal::AuthenticationStatus::Null), - )), - status_code: res.status_code, - }), - ..data.clone() - }), } } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index fbe6a47d2007..8b6a2297d090 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -926,10 +926,22 @@ pub struct PaypalThreeDsResponse { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaypalPreProcessingResponse { +#[serde(untagged)] +pub enum PaypalPreProcessingResponse { + PaypalLiabilityResponse(PaypalLiabilityResponse), + PaypalNonLiablityResponse(PaypalNonLiablityResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalLiabilityResponse { pub payment_source: CardParams, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalNonLiablityResponse { + payment_source: CardsData, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CardParams { pub card: AuthResult, From 00b58a99c819e7827588b2a81460ce56b4df903e Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:51:11 +0530 Subject: [PATCH 127/443] ci(postman): Fixed adyen assertion failure (#3030) Co-authored-by: Likhin Bopanna --- .../Refunds - Create/event.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js index 6731d57fb694..b88beefec22e 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js @@ -50,10 +50,10 @@ if (jsonData?.error?.type) { // Response body should have value "invalid_request" for "error type" if (jsonData?.error?.message) { pm.test( - "[POST]::/payments - Content check if value for 'error.message' matches 'The payment has not succeeded yet. Please pass a successful payment to initiate refund'", + "[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'", function () { pm.expect(jsonData.error.message).to.eql( - "The payment has not succeeded yet. Please pass a successful payment to initiate refund", + "This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured", ); }, ); From c4bd47eca93a158c9daeeeb18afb1e735eea8c94 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:53:48 +0530 Subject: [PATCH 128/443] feat(types): add email types for sending emails (#3020) --- crates/router/src/core/user.rs | 3 +- .../src/services/email/assets/magic_link.html | 32 ++-- crates/router/src/services/email/types.rs | 141 ++++++++++++++++-- 3 files changed, 144 insertions(+), 32 deletions(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 8e7f6c27a7da..7c50e0c7631b 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -81,9 +81,10 @@ pub async fn connect_account( use crate::services::email::types as email_types; - let email_contents = email_types::WelcomeEmail { + let email_contents = email_types::VerifyEmail { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", }; let send_email_result = state diff --git a/crates/router/src/services/email/assets/magic_link.html b/crates/router/src/services/email/assets/magic_link.html index 6439c83f227c..643b6e230633 100644 --- a/crates/router/src/services/email/assets/magic_link.html +++ b/crates/router/src/services/email/assets/magic_link.html @@ -2,20 +2,16 @@ Login to Hyperswitch
Welcome to Hyperswitch!

Dear {user_name},

- We are thrilled to welcome you into our community! + + We are thrilled to welcome you into our community! @@ -140,8 +136,8 @@ align="center" >
- Simply click on the link below, and you'll be granted instant access - to your Hyperswitch account. Note that this link expires in 24 hours + Simply click on the link below, and you'll be granted instant access + to your Hyperswitch account. Note that this link expires in 24 hours and can only be used once.
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 8650e1c27c22..a4a4681c6001 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -5,10 +5,13 @@ use masking::ExposeInterface; use crate::{configs, consts}; #[cfg(feature = "olap")] -use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail}; +use crate::{core::errors::UserErrors, services::jwt, types::domain}; pub enum EmailBody { Verify { link: String }, + Reset { link: String, user_name: String }, + MagicLink { link: String, user_name: String }, + InviteUser { link: String, user_name: String }, } pub mod html { @@ -19,6 +22,27 @@ pub mod html { EmailBody::Verify { link } => { format!(include_str!("assets/verify.html"), link = link) } + EmailBody::Reset { link, user_name } => { + format!( + include_str!("assets/reset.html"), + link = link, + username = user_name + ) + } + EmailBody::MagicLink { link, user_name } => { + format!( + include_str!("assets/magic_link.html"), + user_name = user_name, + link = link + ) + } + EmailBody::InviteUser { link, user_name } => { + format!( + include_str!("assets/invite.html"), + username = user_name, + link = link + ) + } } } } @@ -31,7 +55,7 @@ pub struct EmailToken { impl EmailToken { pub async fn new_token( - email: UserEmail, + email: domain::UserEmail, settings: &configs::settings::Settings, ) -> CustomResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); @@ -44,35 +68,126 @@ impl EmailToken { } } -pub struct WelcomeEmail { - pub recipient_email: UserEmail, - pub settings: std::sync::Arc, -} - -pub fn get_email_verification_link( +pub fn get_link_with_token( base_url: impl std::fmt::Display, token: impl std::fmt::Display, + action: impl std::fmt::Display, ) -> String { - format!("{base_url}/user/verify_email/?token={token}") + format!("{base_url}/user/{action}/?token={token}") +} + +pub struct VerifyEmail { + pub recipient_email: domain::UserEmail, + pub settings: std::sync::Arc, + pub subject: &'static str, } /// Currently only HTML is supported #[async_trait::async_trait] -impl EmailData for WelcomeEmail { +impl EmailData for VerifyEmail { async fn get_email_data(&self) -> CustomResult { let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; - let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token); + let verify_email_link = + get_link_with_token(&self.settings.server.base_url, token, "verify_email"); let body = html::get_html_body(EmailBody::Verify { link: verify_email_link, }); - let subject = "Welcome to the Hyperswitch community!".to_string(); Ok(EmailContents { - subject, + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct ResetPassword { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for ResetPassword { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let reset_password_link = + get_link_with_token(&self.settings.server.base_url, token, "set_password"); + + let body = html::get_html_body(EmailBody::Reset { + link: reset_password_link, + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct MagicLink { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for MagicLink { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let magic_link_login = get_link_with_token(&self.settings.server.base_url, token, "login"); + + let body = html::get_html_body(EmailBody::MagicLink { + link: magic_link_login, + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct InviteUser { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for InviteUser { + async fn get_email_data(&self) -> CustomResult { + let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + .await + .change_context(EmailError::TokenGenerationFailure)?; + + let invite_user_link = + get_link_with_token(&self.settings.server.base_url, token, "set_password"); + + let body = html::get_html_body(EmailBody::MagicLink { + link: invite_user_link, + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), body: external_services::email::IntermediateString::new(body), recipient: self.recipient_email.clone().into_inner(), }) From 092ec73b3c65ce6048d379383b078d643f0f35fc Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:00:25 +0530 Subject: [PATCH 129/443] feat(user): generate and delete sample data (#2987) Co-authored-by: Rachit Naithani Co-authored-by: Mani Chandra Dulam --- crates/api_models/src/events/user.rs | 5 + crates/api_models/src/user.rs | 2 + crates/api_models/src/user/sample_data.rs | 23 ++ crates/diesel_models/src/query/user.rs | 1 + .../src/query/user/sample_data.rs | 139 +++++++++ crates/diesel_models/src/user.rs | 1 + crates/diesel_models/src/user/sample_data.rs | 119 +++++++ crates/router/src/core/errors/user.rs | 1 + .../src/core/errors/user/sample_data.rs | 73 +++++ crates/router/src/core/user.rs | 2 + crates/router/src/core/user/sample_data.rs | 82 +++++ crates/router/src/db.rs | 1 + crates/router/src/db/kafka_store.rs | 118 ++++++- crates/router/src/db/user.rs | 1 + crates/router/src/db/user/sample_data.rs | 205 ++++++++++++ crates/router/src/routes/app.rs | 17 +- crates/router/src/routes/lock_utils.rs | 4 +- crates/router/src/routes/user.rs | 48 ++- crates/router/src/utils/user.rs | 2 + crates/router/src/utils/user/sample_data.rs | 291 ++++++++++++++++++ crates/router_env/src/logger/types.rs | 4 + .../up.sql | 30 +- 22 files changed, 1151 insertions(+), 18 deletions(-) create mode 100644 crates/api_models/src/user/sample_data.rs create mode 100644 crates/diesel_models/src/query/user/sample_data.rs create mode 100644 crates/diesel_models/src/user/sample_data.rs create mode 100644 crates/router/src/core/errors/user/sample_data.rs create mode 100644 crates/router/src/core/user/sample_data.rs create mode 100644 crates/router/src/db/user/sample_data.rs create mode 100644 crates/router/src/utils/user/sample_data.rs diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 50df0c9a584b..3ac65830eb8b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,5 +1,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; +#[cfg(feature = "dummy_connector")] +use crate::user::sample_data::SampleDataRequest; use crate::user::{ dashboard_metadata::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, @@ -29,3 +31,6 @@ common_utils::impl_misc_api_event_type!( CreateInternalUserRequest, UserMerchantCreate ); + +#[cfg(feature = "dummy_connector")] +common_utils::impl_misc_api_event_type!(SampleDataRequest); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e0bfa50b4115..e6e8546c6741 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,6 +1,8 @@ use common_utils::pii; use masking::Secret; pub mod dashboard_metadata; +#[cfg(feature = "dummy_connector")] +pub mod sample_data; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] pub struct ConnectAccountRequest { diff --git a/crates/api_models/src/user/sample_data.rs b/crates/api_models/src/user/sample_data.rs new file mode 100644 index 000000000000..6d20b20f369c --- /dev/null +++ b/crates/api_models/src/user/sample_data.rs @@ -0,0 +1,23 @@ +use common_enums::{AuthenticationType, CountryAlpha2}; +use common_utils::{self}; +use time::PrimitiveDateTime; + +use crate::enums::Connector; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct SampleDataRequest { + pub record: Option, + pub connector: Option>, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub start_time: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub end_time: Option, + // The amount for each sample will be between min_amount and max_amount (in dollars) + pub min_amount: Option, + pub max_amount: Option, + pub currency: Option>, + pub auth_type: Option>, + pub business_country: Option, + pub business_label: Option, + pub profile_id: Option, +} diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index 5761d8af814d..aa1d8471d213 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -1,6 +1,7 @@ use diesel::{associations::HasTable, ExpressionMethods}; use error_stack::report; use router_env::tracing::{self, instrument}; +pub mod sample_data; use crate::{ errors::{self}, diff --git a/crates/diesel_models/src/query/user/sample_data.rs b/crates/diesel_models/src/query/user/sample_data.rs new file mode 100644 index 000000000000..a8ec2c3b0a4f --- /dev/null +++ b/crates/diesel_models/src/query/user/sample_data.rs @@ -0,0 +1,139 @@ +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{associations::HasTable, debug_query, ExpressionMethods, TextExpressionMethods}; +use error_stack::{IntoReport, ResultExt}; +use router_env::logger; + +use crate::{ + errors, + schema::{ + payment_attempt::dsl as payment_attempt_dsl, payment_intent::dsl as payment_intent_dsl, + refund::dsl as refund_dsl, + }, + user::sample_data::PaymentAttemptBatchNew, + PaymentAttempt, PaymentIntent, PaymentIntentNew, PgPooledConn, Refund, RefundNew, + StorageResult, +}; + +pub async fn insert_payment_intents( + conn: &PgPooledConn, + batch: Vec, +) -> StorageResult> { + let query = diesel::insert_into(::table()).values(batch); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while inserting payment intents") +} +pub async fn insert_payment_attempts( + conn: &PgPooledConn, + batch: Vec, +) -> StorageResult> { + let query = diesel::insert_into(::table()).values(batch); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while inserting payment attempts") +} + +pub async fn insert_refunds( + conn: &PgPooledConn, + batch: Vec, +) -> StorageResult> { + let query = diesel::insert_into(::table()).values(batch); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while inserting refunds") +} + +pub async fn delete_payment_intents( + conn: &PgPooledConn, + merchant_id: &str, +) -> StorageResult> { + let query = diesel::delete(::table()) + .filter(payment_intent_dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(payment_intent_dsl::payment_id.like("test_%")); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while deleting payment intents") + .and_then(|result| match result.len() { + n if n > 0 => { + logger::debug!("{n} records deleted"); + Ok(result) + } + 0 => Err(error_stack::report!(errors::DatabaseError::NotFound) + .attach_printable("No records deleted")), + _ => Ok(result), + }) +} +pub async fn delete_payment_attempts( + conn: &PgPooledConn, + merchant_id: &str, +) -> StorageResult> { + let query = diesel::delete(::table()) + .filter(payment_attempt_dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(payment_attempt_dsl::payment_id.like("test_%")); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while deleting payment attempts") + .and_then(|result| match result.len() { + n if n > 0 => { + logger::debug!("{n} records deleted"); + Ok(result) + } + 0 => Err(error_stack::report!(errors::DatabaseError::NotFound) + .attach_printable("No records deleted")), + _ => Ok(result), + }) +} + +pub async fn delete_refunds(conn: &PgPooledConn, merchant_id: &str) -> StorageResult> { + let query = diesel::delete(::table()) + .filter(refund_dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(refund_dsl::payment_id.like("test_%")); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::Others) + .attach_printable("Error while deleting refunds") + .and_then(|result| match result.len() { + n if n > 0 => { + logger::debug!("{n} records deleted"); + Ok(result) + } + 0 => Err(error_stack::report!(errors::DatabaseError::NotFound) + .attach_printable("No records deleted")), + _ => Ok(result), + }) +} diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 4eec710ea185..c608f2654c6a 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -7,6 +7,7 @@ use crate::schema::users; pub mod dashboard_metadata; +pub mod sample_data; #[derive(Clone, Debug, Identifiable, Queryable)] #[diesel(table_name = users)] pub struct User { diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs new file mode 100644 index 000000000000..959d1ad9ee7e --- /dev/null +++ b/crates/diesel_models/src/user/sample_data.rs @@ -0,0 +1,119 @@ +use common_enums::{ + AttemptStatus, AuthenticationType, CaptureMethod, Currency, PaymentExperience, PaymentMethod, + PaymentMethodType, +}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums::MandateDataType, schema::payment_attempt, PaymentAttemptNew}; + +#[derive( + Clone, Debug, Default, diesel::Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, +)] +#[diesel(table_name = payment_attempt)] +pub struct PaymentAttemptBatchNew { + pub payment_id: String, + pub merchant_id: String, + pub attempt_id: String, + pub status: AttemptStatus, + pub amount: i64, + pub currency: Option, + pub save_to_locker: Option, + pub connector: Option, + pub error_message: Option, + pub offer_amount: Option, + pub surcharge_amount: Option, + pub tax_amount: Option, + pub payment_method_id: Option, + pub payment_method: Option, + pub capture_method: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub capture_on: Option, + pub confirm: bool, + pub authentication_type: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub modified_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_synced: Option, + pub cancellation_reason: Option, + pub amount_to_capture: Option, + pub mandate_id: Option, + pub browser_info: Option, + pub payment_token: Option, + pub error_code: Option, + pub connector_metadata: Option, + pub payment_experience: Option, + pub payment_method_type: Option, + pub payment_method_data: Option, + pub business_sub_label: Option, + pub straight_through_algorithm: Option, + pub preprocessing_step_id: Option, + pub mandate_details: Option, + pub error_reason: Option, + pub connector_response_reference_id: Option, + pub connector_transaction_id: Option, + pub multiple_capture_count: Option, + pub amount_capturable: i64, + pub updated_by: String, + pub merchant_connector_id: Option, + pub authentication_data: Option, + pub encoded_data: Option, + pub unified_code: Option, + pub unified_message: Option, +} + +#[allow(dead_code)] +impl PaymentAttemptBatchNew { + // Used to verify compatibility with PaymentAttemptTable + fn convert_into_normal_attempt_insert(self) -> PaymentAttemptNew { + PaymentAttemptNew { + payment_id: self.payment_id, + merchant_id: self.merchant_id, + attempt_id: self.attempt_id, + status: self.status, + amount: self.amount, + currency: self.currency, + save_to_locker: self.save_to_locker, + connector: self.connector, + error_message: self.error_message, + offer_amount: self.offer_amount, + surcharge_amount: self.surcharge_amount, + tax_amount: self.tax_amount, + payment_method_id: self.payment_method_id, + payment_method: self.payment_method, + capture_method: self.capture_method, + capture_on: self.capture_on, + confirm: self.confirm, + authentication_type: self.authentication_type, + created_at: self.created_at, + modified_at: self.modified_at, + last_synced: self.last_synced, + cancellation_reason: self.cancellation_reason, + amount_to_capture: self.amount_to_capture, + mandate_id: self.mandate_id, + browser_info: self.browser_info, + payment_token: self.payment_token, + error_code: self.error_code, + connector_metadata: self.connector_metadata, + payment_experience: self.payment_experience, + payment_method_type: self.payment_method_type, + payment_method_data: self.payment_method_data, + business_sub_label: self.business_sub_label, + straight_through_algorithm: self.straight_through_algorithm, + preprocessing_step_id: self.preprocessing_step_id, + mandate_details: self.mandate_details, + error_reason: self.error_reason, + multiple_capture_count: self.multiple_capture_count, + connector_response_reference_id: self.connector_response_reference_id, + amount_capturable: self.amount_capturable, + updated_by: self.updated_by, + merchant_connector_id: self.merchant_connector_id, + authentication_data: self.authentication_data, + encoded_data: self.encoded_data, + unified_code: self.unified_code, + unified_message: self.unified_message, + } + } +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index ba600917ecca..5e580b003408 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -4,6 +4,7 @@ use crate::services::ApplicationResponse; pub type UserResult = CustomResult; pub type UserResponse = CustomResult, UserErrors>; +pub mod sample_data; #[derive(Debug, thiserror::Error)] pub enum UserErrors { diff --git a/crates/router/src/core/errors/user/sample_data.rs b/crates/router/src/core/errors/user/sample_data.rs new file mode 100644 index 000000000000..11233b27b5cd --- /dev/null +++ b/crates/router/src/core/errors/user/sample_data.rs @@ -0,0 +1,73 @@ +use api_models::errors::types::{ApiError, ApiErrorResponse}; +use common_utils::errors::{CustomResult, ErrorSwitch, ErrorSwitchFrom}; +use data_models::errors::StorageError; + +pub type SampleDataResult = CustomResult; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +pub enum SampleDataError { + #[error["Internal Server Error"]] + InternalServerError, + #[error("Data Does Not Exist")] + DataDoesNotExist, + #[error("Server Error")] + DatabaseError, + #[error("Merchant Id Not Found")] + MerchantIdNotFound, + #[error("Invalid Parameters")] + InvalidParameters, + #[error["Invalid Records"]] + InvalidRange, +} + +impl ErrorSwitch for SampleDataError { + fn switch(&self) -> ApiErrorResponse { + match self { + Self::InternalServerError => ApiErrorResponse::InternalServerError(ApiError::new( + "SD", + 0, + "Something went wrong", + None, + )), + Self::DatabaseError => ApiErrorResponse::InternalServerError(ApiError::new( + "SD", + 1, + "Server Error(DB is down)", + None, + )), + Self::DataDoesNotExist => ApiErrorResponse::NotFound(ApiError::new( + "SD", + 2, + "Sample Data not present for given request", + None, + )), + Self::MerchantIdNotFound => ApiErrorResponse::BadRequest(ApiError::new( + "SD", + 3, + "Merchant ID not provided", + None, + )), + Self::InvalidParameters => ApiErrorResponse::BadRequest(ApiError::new( + "SD", + 4, + "Invalid parameters to generate Sample Data", + None, + )), + Self::InvalidRange => ApiErrorResponse::BadRequest(ApiError::new( + "SD", + 5, + "Records to be generated should be between range 10 and 100", + None, + )), + } + } +} + +impl ErrorSwitchFrom for SampleDataError { + fn switch_from(error: &StorageError) -> Self { + match matches!(error, StorageError::ValueNotFound(_)) { + true => Self::DataDoesNotExist, + false => Self::DatabaseError, + } + } +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 7c50e0c7631b..b38fb4cf4ae3 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -13,6 +13,8 @@ use crate::{ types::domain, utils, }; +#[cfg(feature = "dummy_connector")] +pub mod sample_data; pub mod dashboard_metadata; diff --git a/crates/router/src/core/user/sample_data.rs b/crates/router/src/core/user/sample_data.rs new file mode 100644 index 000000000000..19b7d3bd815c --- /dev/null +++ b/crates/router/src/core/user/sample_data.rs @@ -0,0 +1,82 @@ +use api_models::user::sample_data::SampleDataRequest; +use common_utils::errors::ReportSwitchExt; +use data_models::payments::payment_intent::PaymentIntentNew; +use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; + +pub type SampleDataApiResponse = SampleDataResult>; + +use crate::{ + core::errors::sample_data::SampleDataResult, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + utils::user::sample_data::generate_sample_data, +}; + +pub async fn generate_sample_data_for_user( + state: AppState, + user_from_token: UserFromToken, + req: SampleDataRequest, +) -> SampleDataApiResponse<()> { + let sample_data = + generate_sample_data(&state, req, user_from_token.merchant_id.as_str()).await?; + + let (payment_intents, payment_attempts, refunds): ( + Vec, + Vec, + Vec, + ) = sample_data.into_iter().fold( + (Vec::new(), Vec::new(), Vec::new()), + |(mut pi, mut pa, mut rf), (payment_intent, payment_attempt, refund)| { + pi.push(payment_intent); + pa.push(payment_attempt); + if let Some(refund) = refund { + rf.push(refund); + } + (pi, pa, rf) + }, + ); + + state + .store + .insert_payment_intents_batch_for_sample_data(payment_intents) + .await + .switch()?; + state + .store + .insert_payment_attempts_batch_for_sample_data(payment_attempts) + .await + .switch()?; + state + .store + .insert_refunds_batch_for_sample_data(refunds) + .await + .switch()?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn delete_sample_data_for_user( + state: AppState, + user_from_token: UserFromToken, + _req: SampleDataRequest, +) -> SampleDataApiResponse<()> { + let merchant_id_del = user_from_token.merchant_id.as_str(); + + state + .store + .delete_payment_intents_for_sample_data(merchant_id_del) + .await + .switch()?; + state + .store + .delete_payment_attempts_for_sample_data(merchant_id_del) + .await + .switch()?; + state + .store + .delete_refunds_for_sample_data(merchant_id_del) + .await + .switch()?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 086a09b805c6..6558cc6ace50 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -100,6 +100,7 @@ pub trait StorageInterface: + gsm::GsmInterface + user::UserInterface + user_role::UserRoleInterface + + user::sample_data::BatchSampleDataInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index fcceba7fadba..60a2fb4c2bb3 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -23,7 +23,8 @@ use storage_impl::redis::kv_store::RedisConnInterface; use time::PrimitiveDateTime; use super::{ - dashboard_metadata::DashboardMetadataInterface, user::UserInterface, + dashboard_metadata::DashboardMetadataInterface, + user::{sample_data::BatchSampleDataInterface, UserInterface}, user_role::UserRoleInterface, }; use crate::{ @@ -1951,3 +1952,118 @@ impl DashboardMetadataInterface for KafkaStore { .await } } + +#[async_trait::async_trait] +impl BatchSampleDataInterface for KafkaStore { + async fn insert_payment_intents_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, data_models::errors::StorageError> + { + let payment_intents_list = self + .diesel_store + .insert_payment_intents_batch_for_sample_data(batch) + .await?; + + for payment_intent in payment_intents_list.iter() { + let _ = self + .kafka_producer + .log_payment_intent(payment_intent, None) + .await; + } + Ok(payment_intents_list) + } + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult< + Vec, + data_models::errors::StorageError, + > { + let payment_attempts_list = self + .diesel_store + .insert_payment_attempts_batch_for_sample_data(batch) + .await?; + + for payment_attempt in payment_attempts_list.iter() { + let _ = self + .kafka_producer + .log_payment_attempt(payment_attempt, None) + .await; + } + Ok(payment_attempts_list) + } + + async fn insert_refunds_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, data_models::errors::StorageError> { + let refunds_list = self + .diesel_store + .insert_refunds_batch_for_sample_data(batch) + .await?; + + for refund in refunds_list.iter() { + let _ = self.kafka_producer.log_refund(refund, None).await; + } + Ok(refunds_list) + } + + async fn delete_payment_intents_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, data_models::errors::StorageError> + { + let payment_intents_list = self + .diesel_store + .delete_payment_intents_for_sample_data(merchant_id) + .await?; + + for payment_intent in payment_intents_list.iter() { + let _ = self + .kafka_producer + .log_payment_intent_delete(payment_intent) + .await; + } + Ok(payment_intents_list) + } + + async fn delete_payment_attempts_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult< + Vec, + data_models::errors::StorageError, + > { + let payment_attempts_list = self + .diesel_store + .delete_payment_attempts_for_sample_data(merchant_id) + .await?; + + for payment_attempt in payment_attempts_list.iter() { + let _ = self + .kafka_producer + .log_payment_attempt_delete(payment_attempt) + .await; + } + + Ok(payment_attempts_list) + } + + async fn delete_refunds_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, data_models::errors::StorageError> { + let refunds_list = self + .diesel_store + .delete_refunds_for_sample_data(merchant_id) + .await?; + + for refund in refunds_list.iter() { + let _ = self.kafka_producer.log_refund_delete(refund).await; + } + + Ok(refunds_list) + } +} diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index 6bb1d9e50b6a..be0554ec69a0 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -8,6 +8,7 @@ use crate::{ core::errors::{self, CustomResult}, services::Store, }; +pub mod sample_data; #[async_trait::async_trait] pub trait UserInterface { diff --git a/crates/router/src/db/user/sample_data.rs b/crates/router/src/db/user/sample_data.rs new file mode 100644 index 000000000000..11def9026854 --- /dev/null +++ b/crates/router/src/db/user/sample_data.rs @@ -0,0 +1,205 @@ +use data_models::{ + errors::StorageError, + payments::{payment_attempt::PaymentAttempt, payment_intent::PaymentIntentNew, PaymentIntent}, +}; +use diesel_models::{ + errors::DatabaseError, + query::user::sample_data as sample_data_queries, + refund::{Refund, RefundNew}, + user::sample_data::PaymentAttemptBatchNew, +}; +use error_stack::{Report, ResultExt}; +use storage_impl::DataModelExt; + +use crate::{connection::pg_connection_write, core::errors::CustomResult, services::Store}; + +#[async_trait::async_trait] +pub trait BatchSampleDataInterface { + async fn insert_payment_intents_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError>; + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError>; + + async fn insert_refunds_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError>; + + async fn delete_payment_intents_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError>; + + async fn delete_payment_attempts_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError>; + + async fn delete_refunds_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError>; +} + +#[async_trait::async_trait] +impl BatchSampleDataInterface for Store { + async fn insert_payment_intents_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + let new_intents = batch.into_iter().map(|i| i.to_storage_model()).collect(); + sample_data_queries::insert_payment_intents(&conn, new_intents) + .await + .map_err(diesel_error_to_data_error) + .map(|v| { + v.into_iter() + .map(PaymentIntent::from_storage_model) + .collect() + }) + } + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::insert_payment_attempts(&conn, batch) + .await + .map_err(diesel_error_to_data_error) + .map(|res| { + res.into_iter() + .map(PaymentAttempt::from_storage_model) + .collect() + }) + } + async fn insert_refunds_batch_for_sample_data( + &self, + batch: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::insert_refunds(&conn, batch) + .await + .map_err(diesel_error_to_data_error) + } + + async fn delete_payment_intents_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::delete_payment_intents(&conn, merchant_id) + .await + .map_err(diesel_error_to_data_error) + .map(|v| { + v.into_iter() + .map(PaymentIntent::from_storage_model) + .collect() + }) + } + + async fn delete_payment_attempts_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::delete_payment_attempts(&conn, merchant_id) + .await + .map_err(diesel_error_to_data_error) + .map(|res| { + res.into_iter() + .map(PaymentAttempt::from_storage_model) + .collect() + }) + } + async fn delete_refunds_for_sample_data( + &self, + merchant_id: &str, + ) -> CustomResult, StorageError> { + let conn = pg_connection_write(self) + .await + .change_context(StorageError::DatabaseConnectionError)?; + sample_data_queries::delete_refunds(&conn, merchant_id) + .await + .map_err(diesel_error_to_data_error) + } +} + +#[async_trait::async_trait] +impl BatchSampleDataInterface for storage_impl::MockDb { + async fn insert_payment_intents_batch_for_sample_data( + &self, + _batch: Vec, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + async fn insert_payment_attempts_batch_for_sample_data( + &self, + _batch: Vec, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + async fn insert_refunds_batch_for_sample_data( + &self, + _batch: Vec, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + async fn delete_payment_intents_for_sample_data( + &self, + _merchant_id: &str, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + async fn delete_payment_attempts_for_sample_data( + &self, + _merchant_id: &str, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + async fn delete_refunds_for_sample_data( + &self, + _merchant_id: &str, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } +} + +// TODO: This error conversion is re-used from storage_impl and is not DRY when it should be +// Ideally the impl's here should be defined in that crate avoiding this re-definition +fn diesel_error_to_data_error(diesel_error: Report) -> Report { + let new_err = match diesel_error.current_context() { + DatabaseError::DatabaseConnectionError => StorageError::DatabaseConnectionError, + DatabaseError::NotFound => StorageError::ValueNotFound("Value not found".to_string()), + DatabaseError::UniqueViolation => StorageError::DuplicateValue { + entity: "entity ", + key: None, + }, + DatabaseError::NoFieldsToUpdate => { + StorageError::DatabaseError("No fields to update".to_string()) + } + DatabaseError::QueryGenerationFailed => { + StorageError::DatabaseError("Query generation failed".to_string()) + } + DatabaseError::Others => StorageError::DatabaseError("Others".to_string()), + }; + diesel_error.change_context(new_err) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index d462f4a27390..9c83583bc6af 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -820,8 +820,9 @@ pub struct User; #[cfg(feature = "olap")] impl User { pub fn server(state: AppState) -> Scope { - web::scope("/user") - .app_data(web::Data::new(state)) + let mut route = web::scope("/user").app_data(web::Data::new(state)); + + route = route .service(web::resource("/signin").route(web::post().to(user_connect_account))) .service(web::resource("/signup").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) @@ -842,7 +843,17 @@ impl User { .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) - .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) + .service(web::resource("/role/{role_id}").route(web::get().to(get_role))); + + #[cfg(feature = "dummy_connector")] + { + route = route.service( + web::resource("/sample_data") + .route(web::post().to(generate_sample_data)) + .route(web::delete().to(delete_sample_data)), + ) + } + route } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 552deb85a2e1..04b2b0dc9533 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -155,7 +155,9 @@ impl From for ApiIdentifier { | Flow::VerifyPaymentConnector | Flow::InternalUserSignup | Flow::SwitchMerchant - | Flow::UserMerchantAccountCreate => Self::User, + | Flow::UserMerchantAccountCreate + | Flow::GenerateSampleData + | Flow::DeleteSampleData => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 89c4bd4c90ec..78aecea2444a 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -1,5 +1,10 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::{errors::types::ApiErrorResponse, user as user_api}; +#[cfg(feature = "dummy_connector")] +use api_models::user::sample_data::SampleDataRequest; +use api_models::{ + errors::types::ApiErrorResponse, + user::{self as user_api}, +}; use common_utils::errors::ReportSwitchExt; use router_env::Flow; @@ -158,3 +163,44 @@ pub async fn user_merchant_account_create( )) .await } + +#[cfg(feature = "dummy_connector")] +pub async fn generate_sample_data( + state: web::Data, + http_req: HttpRequest, + payload: web::Json, +) -> impl actix_web::Responder { + use crate::core::user::sample_data; + + let flow = Flow::GenerateSampleData; + Box::pin(api::server_wrap( + flow, + state, + &http_req, + payload.into_inner(), + sample_data::generate_sample_data_for_user, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} +#[cfg(feature = "dummy_connector")] +pub async fn delete_sample_data( + state: web::Data, + http_req: HttpRequest, + payload: web::Json, +) -> impl actix_web::Responder { + use crate::core::user::sample_data; + + let flow = Flow::DeleteSampleData; + Box::pin(api::server_wrap( + flow, + state, + &http_req, + payload.into_inner(), + sample_data::delete_sample_data_for_user, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 4dc54ba3f708..c29e78c7141e 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -9,6 +9,8 @@ use crate::{ pub mod dashboard_metadata; pub mod password; +#[cfg(feature = "dummy_connector")] +pub mod sample_data; impl UserFromToken { pub async fn get_merchant_account(&self, state: AppState) -> UserResult { diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs new file mode 100644 index 000000000000..7a9cf6d2b7db --- /dev/null +++ b/crates/router/src/utils/user/sample_data.rs @@ -0,0 +1,291 @@ +use api_models::{ + enums::Connector::{DummyConnector4, DummyConnector7}, + user::sample_data::SampleDataRequest, +}; +use data_models::payments::payment_intent::PaymentIntentNew; +use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; +use error_stack::{IntoReport, ResultExt}; +use rand::{prelude::SliceRandom, thread_rng, Rng}; +use time::OffsetDateTime; + +use crate::{ + consts, + core::errors::sample_data::{SampleDataError, SampleDataResult}, + AppState, +}; + +#[allow(clippy::type_complexity)] +pub async fn generate_sample_data( + state: &AppState, + req: SampleDataRequest, + merchant_id: &str, +) -> SampleDataResult)>> { + let merchant_id = merchant_id.to_string(); + let sample_data_size: usize = req.record.unwrap_or(100); + + if !(10..=100).contains(&sample_data_size) { + return Err(SampleDataError::InvalidRange.into()); + } + + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id.as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(SampleDataError::DatabaseError)?; + + let merchant_from_db = state + .store + .find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store) + .await + .change_context::(SampleDataError::DataDoesNotExist)?; + + let merchant_parsed_details: Vec = + serde_json::from_value(merchant_from_db.primary_business_details.clone()) + .into_report() + .change_context(SampleDataError::InternalServerError) + .attach_printable("Error while parsing primary business details")?; + + let business_country_default = merchant_parsed_details.get(0).map(|x| x.country); + + let business_label_default = merchant_parsed_details.get(0).map(|x| x.business.clone()); + + let profile_id = crate::core::utils::get_profile_id_from_business_details( + business_country_default, + business_label_default.as_ref(), + &merchant_from_db, + req.profile_id.as_ref(), + &*state.store, + false, + ) + .await + .change_context(SampleDataError::InternalServerError) + .attach_printable("Failed to get business profile")?; + + // 10 percent payments should be failed + #[allow(clippy::as_conversions)] + let failure_attempts = usize::try_from((sample_data_size as f32 / 10.0).round() as i64) + .into_report() + .change_context(SampleDataError::InvalidParameters)?; + + let failure_after_attempts = sample_data_size / failure_attempts; + + // 20 percent refunds for payments + #[allow(clippy::as_conversions)] + let number_of_refunds = usize::try_from((sample_data_size as f32 / 5.0).round() as i64) + .into_report() + .change_context(SampleDataError::InvalidParameters)?; + + let mut refunds_count = 0; + + let mut random_array: Vec = (1..=sample_data_size).collect(); + + // Shuffle the array + let mut rng = thread_rng(); + random_array.shuffle(&mut rng); + + let mut res: Vec<(PaymentIntentNew, PaymentAttemptBatchNew, Option)> = Vec::new(); + let start_time = req + .start_time + .unwrap_or(common_utils::date_time::now() - time::Duration::days(7)) + .assume_utc() + .unix_timestamp(); + let end_time = req + .end_time + .unwrap_or_else(common_utils::date_time::now) + .assume_utc() + .unix_timestamp(); + + let current_time = common_utils::date_time::now().assume_utc().unix_timestamp(); + + let min_amount = req.min_amount.unwrap_or(100); + let max_amount = req.max_amount.unwrap_or(min_amount + 100); + + if min_amount > max_amount + || start_time > end_time + || start_time > current_time + || end_time > current_time + { + return Err(SampleDataError::InvalidParameters.into()); + }; + + let currency_vec = req.currency.unwrap_or(vec![common_enums::Currency::USD]); + let currency_vec_len = currency_vec.len(); + + let connector_vec = req + .connector + .unwrap_or(vec![DummyConnector4, DummyConnector7]); + let connector_vec_len = connector_vec.len(); + + let auth_type = req.auth_type.unwrap_or(vec![ + common_enums::AuthenticationType::ThreeDs, + common_enums::AuthenticationType::NoThreeDs, + ]); + let auth_type_len = auth_type.len(); + + if currency_vec_len == 0 || connector_vec_len == 0 || auth_type_len == 0 { + return Err(SampleDataError::InvalidParameters.into()); + } + + for num in 1..=sample_data_size { + let payment_id = common_utils::generate_id_with_default_len("test"); + let attempt_id = crate::utils::get_payment_attempt_id(&payment_id, 1); + let client_secret = common_utils::generate_id( + consts::ID_LENGTH, + format!("{}_secret", payment_id.clone()).as_str(), + ); + let amount = thread_rng().gen_range(min_amount..=max_amount); + + let created_at @ modified_at @ last_synced = + OffsetDateTime::from_unix_timestamp(thread_rng().gen_range(start_time..=end_time)) + .map(common_utils::date_time::convert_to_pdt) + .unwrap_or( + req.start_time.unwrap_or_else(|| { + common_utils::date_time::now() - time::Duration::days(7) + }), + ); + + // After some set of payments sample data will have a failed attempt + let is_failed_payment = + (random_array.get(num - 1).unwrap_or(&0) % failure_after_attempts) == 0; + + let payment_intent = PaymentIntentNew { + payment_id: payment_id.clone(), + merchant_id: merchant_id.clone(), + status: match is_failed_payment { + true => common_enums::IntentStatus::Failed, + _ => common_enums::IntentStatus::Succeeded, + }, + amount: amount * 100, + currency: Some( + *currency_vec + .get((num - 1) % currency_vec_len) + .unwrap_or(&common_enums::Currency::USD), + ), + description: Some("This is a sample payment".to_string()), + created_at: Some(created_at), + modified_at: Some(modified_at), + last_synced: Some(last_synced), + client_secret: Some(client_secret), + business_country: business_country_default, + business_label: business_label_default.clone(), + active_attempt: data_models::RemoteStorageObject::ForeignID(attempt_id.clone()), + attempt_count: 1, + customer_id: Some("hs-dashboard-user".to_string()), + amount_captured: Some(amount * 100), + profile_id: Some(profile_id.clone()), + return_url: Default::default(), + metadata: Default::default(), + connector_id: Default::default(), + shipping_address_id: Default::default(), + billing_address_id: Default::default(), + statement_descriptor_name: Default::default(), + statement_descriptor_suffix: Default::default(), + setup_future_usage: Default::default(), + off_session: Default::default(), + order_details: Default::default(), + allowed_payment_method_types: Default::default(), + connector_metadata: Default::default(), + feature_metadata: Default::default(), + merchant_decision: Default::default(), + payment_link_id: Default::default(), + payment_confirm_source: Default::default(), + updated_by: merchant_from_db.storage_scheme.to_string(), + surcharge_applicable: Default::default(), + request_incremental_authorization: Default::default(), + incremental_authorization_allowed: Default::default(), + }; + let payment_attempt = PaymentAttemptBatchNew { + attempt_id: attempt_id.clone(), + payment_id: payment_id.clone(), + connector_transaction_id: Some(attempt_id.clone()), + merchant_id: merchant_id.clone(), + status: match is_failed_payment { + true => common_enums::AttemptStatus::Failure, + _ => common_enums::AttemptStatus::Charged, + }, + amount: amount * 100, + currency: payment_intent.currency, + connector: Some( + (*connector_vec + .get((num - 1) % connector_vec_len) + .unwrap_or(&DummyConnector4)) + .to_string(), + ), + payment_method: Some(common_enums::PaymentMethod::Card), + payment_method_type: Some(get_payment_method_type(thread_rng().gen_range(1..=2))), + authentication_type: Some( + *auth_type + .get((num - 1) % auth_type_len) + .unwrap_or(&common_enums::AuthenticationType::NoThreeDs), + ), + error_message: match is_failed_payment { + true => Some("This is a test payment which has a failed status".to_string()), + _ => None, + }, + error_code: match is_failed_payment { + true => Some("HS001".to_string()), + _ => None, + }, + confirm: true, + created_at: Some(created_at), + modified_at: Some(modified_at), + last_synced: Some(last_synced), + amount_to_capture: Some(amount * 100), + connector_response_reference_id: Some(attempt_id.clone()), + updated_by: merchant_from_db.storage_scheme.to_string(), + + ..Default::default() + }; + + let refund = if refunds_count < number_of_refunds && !is_failed_payment { + refunds_count += 1; + Some(RefundNew { + refund_id: common_utils::generate_id_with_default_len("test"), + internal_reference_id: common_utils::generate_id_with_default_len("test"), + external_reference_id: None, + payment_id: payment_id.clone(), + attempt_id: attempt_id.clone(), + merchant_id: merchant_id.clone(), + connector_transaction_id: attempt_id.clone(), + connector_refund_id: None, + description: Some("This is a sample refund".to_string()), + created_at: Some(created_at), + modified_at: Some(modified_at), + refund_reason: Some("Sample Refund".to_string()), + connector: payment_attempt + .connector + .clone() + .unwrap_or(DummyConnector4.to_string()), + currency: *currency_vec + .get((num - 1) % currency_vec_len) + .unwrap_or(&common_enums::Currency::USD), + total_amount: amount * 100, + refund_amount: amount * 100, + refund_status: common_enums::RefundStatus::Success, + sent_to_gateway: true, + refund_type: diesel_models::enums::RefundType::InstantRefund, + metadata: None, + refund_arn: None, + profile_id: payment_intent.profile_id.clone(), + updated_by: merchant_from_db.storage_scheme.to_string(), + merchant_connector_id: payment_attempt.merchant_connector_id.clone(), + }) + } else { + None + }; + + res.push((payment_intent, payment_attempt, refund)); + } + Ok(res) +} + +fn get_payment_method_type(num: u8) -> common_enums::PaymentMethodType { + let rem: u8 = (num) % 2; + match rem { + 0 => common_enums::PaymentMethodType::Debit, + _ => common_enums::PaymentMethodType::Credit, + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index eefdc86affad..c844a6aeded6 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -279,6 +279,10 @@ pub enum Flow { UpdateUserRole, /// Create merchant account for user in a org UserMerchantAccountCreate, + /// Generate Sample Data + GenerateSampleData, + /// Delete Sample Data + DeleteSampleData, } /// diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql index 8296f755f543..4a74afb9ad0e 100644 --- a/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql @@ -1,15 +1,21 @@ -- Your SQL goes here + CREATE TABLE IF NOT EXISTS dashboard_metadata ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(64), - merchant_id VARCHAR(64) NOT NULL, - org_id VARCHAR(64) NOT NULL, - data_key VARCHAR(64) NOT NULL, - data_value JSON NOT NULL, - created_by VARCHAR(64) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - last_modified_by VARCHAR(64) NOT NULL, - last_modified_at TIMESTAMP NOT NULL DEFAULT now() -); + id SERIAL PRIMARY KEY, + user_id VARCHAR(64), + merchant_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + data_key VARCHAR(64) NOT NULL, + data_value JSON NOT NULL, + created_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_by VARCHAR(64) NOT NULL, + last_modified_at TIMESTAMP NOT NULL DEFAULT now() + ); -CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (COALESCE(user_id,'0'), merchant_id, org_id, data_key); \ No newline at end of file +CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata ( + COALESCE(user_id, '0'), + merchant_id, + org_id, + data_key +); \ No newline at end of file From 95876b0ce03e024edf77909502c53eb4e63a9855 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:48:40 +0530 Subject: [PATCH 130/443] =?UTF-8?q?feat(connector):=20[BOA/CYBERSOURCE]=20?= =?UTF-8?q?Fix=20Status=20Mapping=20for=20Terminal=20St=E2=80=A6=20(#3031)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../router/src/connector/bankofamerica/transformers.rs | 10 ++++++++-- .../router/src/connector/cybersource/transformers.rs | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 18ec8ceb89d9..e31a69669c6d 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -442,11 +442,18 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { | BankofamericaPaymentStatus::AuthorizedPendingReview => { if auto_capture { // Because BankOfAmerica will return Payment Status as Authorized even in AutoCapture Payment - Self::Pending + Self::Charged } else { Self::Authorized } } + BankofamericaPaymentStatus::Pending => { + if auto_capture { + Self::Charged + } else { + Self::Pending + } + } BankofamericaPaymentStatus::Succeeded | BankofamericaPaymentStatus::Transmitted => { Self::Charged } @@ -456,7 +463,6 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { BankofamericaPaymentStatus::Failed | BankofamericaPaymentStatus::Declined => { Self::Failure } - BankofamericaPaymentStatus::Pending => Self::Pending, } } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 495e23e001ad..953f82c76a83 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -670,8 +670,9 @@ pub struct ApplicationInformation { fn get_payment_status(is_capture: bool, status: enums::AttemptStatus) -> enums::AttemptStatus { let is_authorized = matches!(status, enums::AttemptStatus::Authorized); - if is_capture && is_authorized { - return enums::AttemptStatus::Pending; + let is_pending = matches!(status, enums::AttemptStatus::Pending); + if is_capture && (is_authorized || is_pending) { + return enums::AttemptStatus::Charged; } status } From ec15ddd0d0ed942fedec525406df3005d494b8d4 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:07:17 +0530 Subject: [PATCH 131/443] feat(user): add user_list and switch_list apis (#3033) Co-authored-by: Mani Chandra Dulam --- crates/api_models/src/events/user.rs | 5 +- crates/api_models/src/user.rs | 17 +++++ crates/api_models/src/user_role.rs | 6 ++ .../src/query/dashboard_metadata.rs | 30 ++++++++- crates/diesel_models/src/query/user.rs | 47 +++++++++++--- .../src/user/dashboard_metadata.rs | 37 +++++++++++ crates/router/src/core/user.rs | 26 ++++++++ crates/router/src/db/dashboard_metadata.rs | 65 +++++++++++++++++++ crates/router/src/db/kafka_store.rs | 28 ++++++++ crates/router/src/db/user.rs | 60 ++++++----------- crates/router/src/routes/app.rs | 2 + crates/router/src/routes/lock_utils.rs | 4 +- crates/router/src/routes/user.rs | 31 +++++++++ crates/router/src/types/domain/user.rs | 29 ++++++++- crates/router/src/utils/user.rs | 17 +++++ crates/router_env/src/logger/types.rs | 4 ++ 16 files changed, 356 insertions(+), 52 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 3ac65830eb8b..8b7cd02c9350 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -7,7 +7,7 @@ use crate::user::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, - CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate, + CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate, }; impl ApiEventMetric for ConnectAccountResponse { @@ -29,7 +29,8 @@ common_utils::impl_misc_api_event_type!( SetMetaDataRequest, SwitchMerchantIdRequest, CreateInternalUserRequest, - UserMerchantCreate + UserMerchantCreate, + GetUsersResponse ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e6e8546c6741..36d730f5118e 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,5 +1,7 @@ use common_utils::pii; use masking::Secret; + +use crate::user_role::UserStatus; pub mod dashboard_metadata; #[cfg(feature = "dummy_connector")] pub mod sample_data; @@ -45,3 +47,18 @@ pub struct CreateInternalUserRequest { pub struct UserMerchantCreate { pub company_name: String, } + +#[derive(Debug, serde::Serialize)] +pub struct GetUsersResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct UserDetails { + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub role_id: String, + pub role_name: String, + pub status: UserStatus, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: time::PrimitiveDateTime, +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 521d17e73428..735cd240b6e7 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -80,3 +80,9 @@ pub struct UpdateUserRoleRequest { pub user_id: String, pub role_id: String, } + +#[derive(Debug, serde::Serialize)] +pub enum UserStatus { + Active, + InvitationSent, +} diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index 03e4a2dab38b..44fd24c7acf2 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -5,7 +5,10 @@ use crate::{ enums, query::generics, schema::dashboard_metadata::dsl, - user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + user::dashboard_metadata::{ + DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate, + DashboardMetadataUpdateInternal, + }, PgPooledConn, StorageResult, }; @@ -17,6 +20,31 @@ impl DashboardMetadataNew { } impl DashboardMetadata { + pub async fn update( + conn: &PgPooledConn, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: DashboardMetadataUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::user_id + .eq(user_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_owned())) + .and(dsl::org_id.eq(org_id.to_owned())) + .and(dsl::data_key.eq(data_key.to_owned())), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } + pub async fn find_user_scoped_dashboard_metadata( conn: &PgPooledConn, user_id: String, diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index aa1d8471d213..b4d5976ba294 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -1,13 +1,24 @@ -use diesel::{associations::HasTable, ExpressionMethods}; -use error_stack::report; -use router_env::tracing::{self, instrument}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{ + associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, + JoinOnDsl, QueryDsl, +}; +use error_stack::{report, IntoReport}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; pub mod sample_data; use crate::{ errors::{self}, query::generics, - schema::users::dsl, + schema::{ + user_roles::{self, dsl as user_roles_dsl}, + users::dsl as users_dsl, + }, user::*, + user_role::UserRole, PgPooledConn, StorageResult, }; @@ -22,7 +33,7 @@ impl User { pub async fn find_by_user_email(conn: &PgPooledConn, user_email: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::email.eq(user_email.to_owned()), + users_dsl::email.eq(user_email.to_owned()), ) .await } @@ -30,7 +41,7 @@ impl User { pub async fn find_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), ) .await } @@ -42,7 +53,7 @@ impl User { ) -> StorageResult { generics::generic_update_with_results::<::Table, _, _, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), UserUpdateInternal::from(user), ) .await? @@ -56,8 +67,28 @@ impl User { pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { generics::generic_delete::<::Table, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), ) .await } + + pub async fn find_joined_users_and_roles_by_merchant_id( + conn: &PgPooledConn, + mid: &str, + ) -> StorageResult> { + let query = Self::table() + .inner_join(user_roles::table.on(user_roles_dsl::user_id.eq(users_dsl::user_id))) + .filter(user_roles_dsl::merchant_id.eq(mid.to_owned())); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async::<(Self, UserRole)>(conn) + .await + .into_report() + .map_err(|err| match err.current_context() { + DieselError::NotFound => err.change_context(errors::DatabaseError::NotFound), + _ => err.change_context(errors::DatabaseError::Others), + }) + } } diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs index 018808f1c0db..1eeb61d6135e 100644 --- a/crates/diesel_models/src/user/dashboard_metadata.rs +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -33,3 +33,40 @@ pub struct DashboardMetadataNew { pub last_modified_by: String, pub last_modified_at: PrimitiveDateTime, } + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataUpdateInternal { + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +pub enum DashboardMetadataUpdate { + UpdateData { + data_key: enums::DashboardMetadata, + data_value: serde_json::Value, + last_modified_by: String, + }, +} + +impl From for DashboardMetadataUpdateInternal { + fn from(metadata_update: DashboardMetadataUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match metadata_update { + DashboardMetadataUpdate::UpdateData { + data_key, + data_value, + last_modified_by, + } => Self { + data_key, + data_value, + last_modified_by, + last_modified_at, + }, + } + } +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index b38fb4cf4ae3..7d0d599cc4ed 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -324,3 +324,29 @@ pub async fn create_merchant_account( Ok(ApplicationResponse::StatusOk) } + +pub async fn list_merchant_ids_for_user( + state: AppState, + user: auth::UserFromToken, +) -> UserResponse> { + Ok(ApplicationResponse::Json( + utils::user::get_merchant_ids_for_user(state, &user.user_id).await?, + )) +} + +pub async fn get_users_for_merchant_account( + state: AppState, + user_from_token: auth::UserFromToken, +) -> UserResponse { + let users = state + .store + .find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("No users for given merchant id")? + .into_iter() + .filter_map(|(user, role)| domain::UserAndRoleJoined(user, role).try_into().ok()) + .collect(); + + Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) +} diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs index 2e8129398ca3..ec24b4ed07da 100644 --- a/crates/router/src/db/dashboard_metadata.rs +++ b/crates/router/src/db/dashboard_metadata.rs @@ -14,6 +14,14 @@ pub trait DashboardMetadataInterface { &self, metadata: storage::DashboardMetadataNew, ) -> CustomResult; + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult; async fn find_user_scoped_dashboard_metadata( &self, @@ -44,6 +52,28 @@ impl DashboardMetadataInterface for Store { .into_report() } + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::update( + &conn, + user_id, + merchant_id, + org_id, + data_key, + dashboard_metadata_update, + ) + .await + .map_err(Into::into) + .into_report() + } + async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, @@ -121,6 +151,41 @@ impl DashboardMetadataInterface for MockDb { Ok(metadata_new) } + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + + let dashboard_metadata_to_update = dashboard_metadata + .iter_mut() + .find(|metadata| { + metadata.user_id == user_id + && metadata.merchant_id == merchant_id + && metadata.org_id == org_id + && metadata.data_key == data_key + }) + .ok_or(errors::StorageError::MockDbError)?; + + match dashboard_metadata_update { + storage::DashboardMetadataUpdate::UpdateData { + data_key, + data_value, + last_modified_by, + } => { + dashboard_metadata_to_update.data_key = data_key; + dashboard_metadata_to_update.data_value = data_value; + dashboard_metadata_to_update.last_modified_by = last_modified_by; + dashboard_metadata_to_update.last_modified_at = common_utils::date_time::now(); + } + } + Ok(dashboard_metadata_to_update.clone()) + } + async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 60a2fb4c2bb3..32548e36b6fb 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1878,6 +1878,15 @@ impl UserInterface for KafkaStore { ) -> CustomResult { self.diesel_store.delete_user_by_user_id(user_id).await } + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_users_and_roles_by_merchant_id(merchant_id) + .await + } } impl RedisConnInterface for KafkaStore { @@ -1930,6 +1939,25 @@ impl DashboardMetadataInterface for KafkaStore { self.diesel_store.insert_metadata(metadata).await } + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + self.diesel_store + .update_metadata( + user_id, + merchant_id, + org_id, + data_key, + dashboard_metadata_update, + ) + .await + } + async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index be0554ec69a0..e3dda965f9c9 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -1,4 +1,4 @@ -use diesel_models::user as storage; +use diesel_models::{user as storage, user_role::UserRole}; use error_stack::{IntoReport, ResultExt}; use masking::Secret; @@ -37,6 +37,11 @@ pub trait UserInterface { &self, user_id: &str, ) -> CustomResult; + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -97,6 +102,17 @@ impl UserInterface for Store { .map_err(Into::into) .into_report() } + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_joined_users_and_roles_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -222,45 +238,11 @@ impl UserInterface for MockDb { users.remove(user_index); Ok(true) } -} -#[cfg(feature = "kafka_events")] -#[async_trait::async_trait] -impl UserInterface for super::KafkaStore { - async fn insert_user( - &self, - user_data: storage::UserNew, - ) -> CustomResult { - self.diesel_store.insert_user(user_data).await - } - async fn find_user_by_email( + async fn find_users_and_roles_by_merchant_id( &self, - user_email: &str, - ) -> CustomResult { - self.diesel_store.find_user_by_email(user_email).await - } - - async fn find_user_by_id( - &self, - user_id: &str, - ) -> CustomResult { - self.diesel_store.find_user_by_id(user_id).await - } - - async fn update_user_by_user_id( - &self, - user_id: &str, - user: storage::UserUpdate, - ) -> CustomResult { - self.diesel_store - .update_user_by_user_id(user_id, user) - .await - } - - async fn delete_user_by_user_id( - &self, - user_id: &str, - ) -> CustomResult { - self.diesel_store.delete_user_by_user_id(user_id).await + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 9c83583bc6af..a145f3e7e5d7 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -839,6 +839,8 @@ impl User { web::resource("/create_merchant") .route(web::post().to(user_merchant_account_create)), ) + .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) + .service(web::resource("/user/list").route(web::get().to(get_user_details))) // User Role APIs .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 04b2b0dc9533..6aa2bbad0b15 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -157,7 +157,9 @@ impl From for ApiIdentifier { | Flow::SwitchMerchant | Flow::UserMerchantAccountCreate | Flow::GenerateSampleData - | Flow::DeleteSampleData => Self::User, + | Flow::DeleteSampleData + | Flow::UserMerchantAccountList + | Flow::GetUserDetails => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 78aecea2444a..97bd7054da9e 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -204,3 +204,34 @@ pub async fn delete_sample_data( )) .await } + +pub async fn list_merchant_ids_for_user( + state: web::Data, + req: HttpRequest, +) -> HttpResponse { + let flow = Flow::UserMerchantAccountList; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, user, _| user_core::list_merchant_ids_for_user(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_user_details(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::GetUserDetails; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user, _| user_core::get_users_for_merchant_account(state, user), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 0c7760f84d36..082b29d80941 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -27,7 +27,7 @@ use crate::{ routes::AppState, services::{ authentication::{AuthToken, UserFromToken}, - authorization::info, + authorization::{info, predefined_permissions}, }, types::transformers::ForeignFrom, utils::user::password, @@ -671,3 +671,30 @@ impl TryFrom for user_role_api::PermissionInfo { }) } } + +pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole); + +impl TryFrom for user_api::UserDetails { + type Error = (); + fn try_from(user_and_role: UserAndRoleJoined) -> Result { + let status = match user_and_role.1.status { + UserStatus::Active => user_role_api::UserStatus::Active, + UserStatus::InvitationSent => user_role_api::UserStatus::InvitationSent, + }; + + let role_id = user_and_role.1.role_id; + let role_name = predefined_permissions::get_role_name_from_id(role_id.as_str()) + .ok_or(())? + .to_string(); + + Ok(Self { + user_id: user_and_role.0.user_id, + email: user_and_role.0.email, + name: user_and_role.0.name, + role_id, + status, + role_name, + last_modified_at: user_and_role.1.last_modified_at, + }) + } +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c29e78c7141e..696aa4090044 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,3 +1,4 @@ +use diesel_models::enums::UserStatus; use error_stack::ResultExt; use crate::{ @@ -51,3 +52,19 @@ impl UserFromToken { Ok(user) } } + +pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult> { + Ok(state + .store + .list_user_roles_by_user_id(user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|ele| { + if ele.status == UserStatus::Active { + return Some(ele.merchant_id); + } + None + }) + .collect()) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index c844a6aeded6..f54a5a82baaf 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -283,6 +283,10 @@ pub enum Flow { GenerateSampleData, /// Delete Sample Data DeleteSampleData, + /// List merchant accounts for user + UserMerchantAccountList, + /// Get users for merchant account + GetUserDetails, } /// From 83fcd1a9deb106a44c8262923c7f1660b0c46bf2 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:20:41 +0530 Subject: [PATCH 132/443] feat(router): add key_value to locker metrics (#2995) --- .../router/src/core/payment_methods/cards.rs | 84 ++++++++++++++++--- crates/router/src/routes/metrics.rs | 1 + 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 044e270a7ea9..545733e298ab 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -225,12 +225,21 @@ pub async fn add_card_to_locker( ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) .await?; logger::debug!("card added to basilisk locker"); @@ -248,22 +257,45 @@ pub async fn add_card_to_locker( ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], ) .await; match add_card_to_rs_resp { value @ Ok(_) => { - logger::debug!("Card added successfully"); + logger::debug!("card added to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); value } Err(err) => { - logger::debug!(error =? err,"failed to add card"); + logger::debug!(error =? err,"failed to add card to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); Ok(add_card_to_hs_resp) } } @@ -290,12 +322,19 @@ pub async fn get_card_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) }, &metrics::CARD_GET_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], ) .await; @@ -313,20 +352,45 @@ pub async fn get_card_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) }, &metrics::CARD_GET_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) .await .map(|inner_card| { logger::debug!("card retrieved from basilisk locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); inner_card }), Ok(_) => { logger::debug!("card retrieved from rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); get_card_from_rs_locker_resp } } diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index a8e6f9d2a892..192df1a09298 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -85,6 +85,7 @@ counter_metric!(CONNECTOR_HTTP_STATUS_CODE_5XX_COUNT, GLOBAL_METER); // Service Level counter_metric!(CARD_LOCKER_FAILURES, GLOBAL_METER); +counter_metric!(CARD_LOCKER_SUCCESSFUL_RESPONSE, GLOBAL_METER); counter_metric!(TEMP_LOCKER_FAILURES, GLOBAL_METER); histogram_metric!(CARD_ADD_TIME, GLOBAL_METER); histogram_metric!(CARD_GET_TIME, GLOBAL_METER); From 3ce04abae4eddfa27025368f5ef28987cccea43d Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:40:43 +0530 Subject: [PATCH 133/443] refactor(payment_methods): add support for passing card_cvc in payment_method_data object along with token (#3024) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 11 ++++-- crates/router/src/core/payment_methods.rs | 6 --- crates/router/src/core/payments/helpers.rs | 43 ++++++++++++++-------- openapi/openapi_spec.json | 11 +++++- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index fe5ed417f350..49f2781a18a0 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -204,8 +204,9 @@ pub struct PaymentsRequest { #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] pub payment_token: Option, - /// This is used when payment is to be confirmed and the card is not saved - #[schema(value_type = Option)] + /// This is used when payment is to be confirmed and the card is not saved. + /// This field will be deprecated soon, use the CardToken object instead + #[schema(value_type = Option, deprecated)] pub card_cvc: Option>, /// The shipping address for the payment @@ -720,12 +721,16 @@ pub struct Card { pub nick_name: Option>, } -#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, Default)] #[serde(rename_all = "snake_case")] pub struct CardToken { /// The card holder's name #[schema(value_type = String, example = "John Test")] pub card_holder_name: Option>, + + /// The CVC number for the card + #[schema(value_type = Option)] + pub card_cvc: Option>, } #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 1049137a9470..a2dbfb1480c4 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -42,7 +42,6 @@ pub trait PaymentMethodRetrieve { key_store: &domain::MerchantKeyStore, token: &storage::PaymentTokenData, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult>; } @@ -126,7 +125,6 @@ impl PaymentMethodRetrieve for Oss { merchant_key_store: &domain::MerchantKeyStore, token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult> { match token_data { @@ -135,7 +133,6 @@ impl PaymentMethodRetrieve for Oss { state, &generic_token.token, payment_intent, - card_cvc, merchant_key_store, card_token_data, ) @@ -147,7 +144,6 @@ impl PaymentMethodRetrieve for Oss { state, &generic_token.token, payment_intent, - card_cvc, merchant_key_store, card_token_data, ) @@ -159,7 +155,6 @@ impl PaymentMethodRetrieve for Oss { state, &card_token.token, payment_intent, - card_cvc, card_token_data, ) .await @@ -171,7 +166,6 @@ impl PaymentMethodRetrieve for Oss { state, &card_token.token, payment_intent, - card_cvc, card_token_data, ) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4d11f6400f44..0cce91bebeeb 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1354,7 +1354,6 @@ pub async fn retrieve_payment_method_with_temporary_token( state: &AppState, token: &str, payment_intent: &PaymentIntent, - card_cvc: Option>, merchant_key_store: &domain::MerchantKeyStore, card_token_data: Option<&CardToken>, ) -> RouterResult> { @@ -1395,10 +1394,13 @@ pub async fn retrieve_payment_method_with_temporary_token( updated_card.card_holder_name = name_on_card; } - if let Some(cvc) = card_cvc { - is_card_updated = true; - updated_card.card_cvc = cvc; + if let Some(token_data) = card_token_data { + if let Some(cvc) = token_data.card_cvc.clone() { + is_card_updated = true; + updated_card.card_cvc = cvc; + } } + if is_card_updated { let updated_pm = api::PaymentMethodData::Card(updated_card); vault::Vault::store_payment_method_data_in_locker( @@ -1444,7 +1446,6 @@ pub async fn retrieve_card_with_permanent_token( state: &AppState, token: &str, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult { let customer_id = payment_intent @@ -1479,7 +1480,11 @@ pub async fn retrieve_card_with_permanent_token( card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, - card_cvc: card_cvc.unwrap_or_default(), + card_cvc: card_token_data + .cloned() + .unwrap_or_default() + .card_cvc + .unwrap_or_default(), card_issuer: card.card_brand, nick_name: card.nick_name.map(masking::Secret::new), card_network: None, @@ -1501,6 +1506,22 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( Option, )> { let request = &payment_data.payment_method_data.clone(); + + let mut card_token_data = payment_data + .payment_method_data + .clone() + .and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }) + .or(Some(CardToken::default())); + + if let Some(cvc) = payment_data.card_cvc.clone() { + if let Some(token_data) = card_token_data.as_mut() { + token_data.card_cvc = Some(cvc); + } + } + let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { @@ -1560,13 +1581,6 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( } }; - let card_cvc = payment_data.card_cvc.clone(); - - let card_token_data = request.as_ref().and_then(|pmd| match pmd { - api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), - _ => None, - }); - // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { @@ -1575,8 +1589,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( merchant_key_store, &hyperswitch_token, &payment_data.payment_intent, - card_cvc, - card_token_data, + card_token_data.as_ref(), ) .await .attach_printable("in 'make_pm_data'")?; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index f5ad99f05752..b1e313f15baa 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4316,6 +4316,11 @@ "type": "string", "description": "The card holder's name", "example": "John Test" + }, + "card_cvc": { + "type": "string", + "description": "The CVC number for the card", + "nullable": true } } }, @@ -9545,7 +9550,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "deprecated": true, "nullable": true }, "shipping": { @@ -9914,7 +9920,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "deprecated": true, "nullable": true }, "shipping": { From 9d935332193dcc9f191a0a5a9e7405316794a418 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:39:33 +0530 Subject: [PATCH 134/443] feat(pm_list): Add required field for open_banking_uk for Adyen and Volt Connector (#3032) Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- crates/router/src/configs/defaults.rs | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index f9bfcae1ca10..db29e8180089 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4106,6 +4106,58 @@ impl Default for super::settings::RequiredFields { ( enums::PaymentMethod::BankRedirect, PaymentMethodType(HashMap::from([ + ( + enums::PaymentMethodType::OpenBankingUk, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Volt, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap:: from([ + ( + "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + display_name: "issuer".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ) + ]), + }, + ), ( enums::PaymentMethodType::Przelewy24, ConnectorFields { From 80efeb76b1801529766978af1c06e2d2c7de66c0 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:03:44 +0530 Subject: [PATCH 135/443] refactor(users): Separate signup and signin (#2921) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 15 +- crates/api_models/src/user.rs | 40 ++++- crates/router/src/core/user.rs | 215 ++++++++++++++++++------- crates/router/src/routes/app.rs | 21 ++- crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/user.rs | 59 +++++++ crates/router/src/types/domain/user.rs | 144 ++++++++++++----- crates/router/src/utils/user.rs | 57 ++++++- crates/router_env/src/logger/types.rs | 6 + 9 files changed, 453 insertions(+), 109 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 8b7cd02c9350..3634b51e0cc0 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -6,11 +6,12 @@ use crate::user::{ dashboard_metadata::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, - ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, - CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate, + AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, + DashboardEntryResponse, GetUsersResponse, SignUpRequest, SignUpWithMerchantIdRequest, + SwitchMerchantIdRequest, UserMerchantCreate, }; -impl ApiEventMetric for ConnectAccountResponse { +impl ApiEventMetric for DashboardEntryResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::User { merchant_id: self.merchant_id.clone(), @@ -19,9 +20,9 @@ impl ApiEventMetric for ConnectAccountResponse { } } -impl ApiEventMetric for ConnectAccountRequest {} - common_utils::impl_misc_api_event_type!( + SignUpRequest, + SignUpWithMerchantIdRequest, ChangePasswordRequest, GetMultipleMetaDataPayload, GetMetaDataResponse, @@ -30,7 +31,9 @@ common_utils::impl_misc_api_event_type!( SwitchMerchantIdRequest, CreateInternalUserRequest, UserMerchantCreate, - GetUsersResponse + GetUsersResponse, + AuthorizeResponse, + ConnectAccountRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 36d730f5118e..287c377eb46a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -7,13 +7,25 @@ pub mod dashboard_metadata; pub mod sample_data; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] -pub struct ConnectAccountRequest { +pub struct SignUpWithMerchantIdRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, + pub company_name: String, +} + +pub type SignUpWithMerchantIdResponse = AuthorizeResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct SignUpRequest { pub email: pii::Email, pub password: Secret, } +pub type SignUpResponse = DashboardEntryResponse; + #[derive(serde::Serialize, Debug, Clone)] -pub struct ConnectAccountResponse { +pub struct DashboardEntryResponse { pub token: Secret, pub merchant_id: String, pub name: Secret, @@ -25,6 +37,28 @@ pub struct ConnectAccountResponse { pub user_id: String, } +pub type SignInRequest = SignUpRequest; + +pub type SignInResponse = DashboardEntryResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct ConnectAccountRequest { + pub email: pii::Email, +} + +pub type ConnectAccountResponse = AuthorizeResponse; + +#[derive(serde::Serialize, Debug, Clone)] +pub struct AuthorizeResponse { + pub is_email_sent: bool, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub user_id: String, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub merchant_id: String, +} + #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct ChangePasswordRequest { pub new_password: Secret, @@ -36,6 +70,8 @@ pub struct SwitchMerchantIdRequest { pub merchant_id: String, } +pub type SwitchMerchantResponse = DashboardEntryResponse; + #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct CreateInternalUserRequest { pub name: Secret, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 7d0d599cc4ed..c868530f81af 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,10 +1,17 @@ use api_models::user as user_api; use diesel_models::{enums::UserStatus, user as storage_user}; -use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, Secret}; +#[cfg(feature = "email")] +use error_stack::IntoReport; +use error_stack::ResultExt; +use masking::ExposeInterface; +#[cfg(feature = "email")] use router_env::env; +#[cfg(feature = "email")] +use router_env::logger; use super::errors::{UserErrors, UserResponse}; +#[cfg(feature = "email")] +use crate::services::email::types as email_types; use crate::{ consts, db::user::UserInterface, @@ -13,11 +20,112 @@ use crate::{ types::domain, utils, }; +pub mod dashboard_metadata; #[cfg(feature = "dummy_connector")] pub mod sample_data; -pub mod dashboard_metadata; +#[cfg(feature = "email")] +pub async fn signup_with_merchant_id( + state: AppState, + request: user_api::SignUpWithMerchantIdRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request.clone())?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + + let email_contents = email_types::ResetPassword { + recipient_email: user_from_db.get_email().try_into()?, + user_name: domain::UserName::new(user_from_db.get_name())?, + settings: state.conf.clone(), + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + Ok(ApplicationResponse::Json(user_api::AuthorizeResponse { + is_email_sent: send_email_result.is_ok(), + user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, + })) +} + +pub async fn signup( + state: AppState, + request: user_api::SignUpRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request)?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + )) +} + +pub async fn signin( + state: AppState, + request: user_api::SignInRequest, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidCredentials) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + user_from_db.compare_password(request.password)?; + + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + )) +} + +#[cfg(feature = "email")] pub async fn connect_account( state: AppState, request: user_api::ConnectAccountRequest, @@ -29,26 +137,34 @@ pub async fn connect_account( if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; - user_from_db.compare_password(request.password)?; + let email_contents = email_types::MagicLink { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + user_name: domain::UserName::new(user_from_db.get_name())?, + subject: "Unlock Hyperswitch: Use Your Magic Link to Sign In", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; - let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; + logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, + is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, }, )); } else if find_user + .as_ref() .map_err(|e| e.current_context().is_db_not_found()) .err() .unwrap_or(false) @@ -73,46 +189,35 @@ pub async fn connect_account( UserStatus::Active, ) .await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; - - #[cfg(feature = "email")] - { - use router_env::logger; - - use crate::services::email::types as email_types; - - let email_contents = email_types::VerifyEmail { - recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, - settings: state.conf.clone(), - subject: "Welcome to the Hyperswitch community!", - }; - let send_email_result = state - .email_client - .compose_and_send_email( - Box::new(email_contents), - state.conf.proxy.https_url.as_ref(), - ) - .await; + let email_contents = email_types::VerifyEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; - logger::info!(?send_email_result); - } + logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, + is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, }, )); } else { - Err(UserErrors::InternalServerError.into()) + Err(find_user + .err() + .map(|e| e.change_context(UserErrors::InternalServerError)) + .unwrap_or(UserErrors::InternalServerError.into())) } } @@ -215,7 +320,7 @@ pub async fn switch_merchant_id( state: AppState, request: user_api::SwitchMerchantIdRequest, user_from_token: auth::UserFromToken, -) -> UserResponse { +) -> UserResponse { if !utils::user_role::is_internal_role(&user_from_token.role_id) { let merchant_list = utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) @@ -252,7 +357,7 @@ pub async fn switch_merchant_id( } })?; - let org_id = state + let _org_id = state .store .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) .await @@ -272,23 +377,23 @@ pub async fn switch_merchant_id( .await .change_context(UserErrors::InternalServerError)?; - let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id( - state.clone(), + let token = utils::user::generate_jwt_auth_token_with_custom_merchant_id( + state, + &user, + &user_role, request.merchant_id.clone(), - org_id, - )) - .await? - .into(); + ) + .await?; Ok(ApplicationResponse::Json( - user_api::ConnectAccountResponse { - merchant_id: request.merchant_id, + user_api::SwitchMerchantResponse { token, name: user.get_name(), email: user.get_email(), user_id: user.get_user_id().to_string(), verification_days_left: None, user_role: user_role.role_id, + merchant_id: user_role.merchant_id, }, )) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a145f3e7e5d7..88806b565d3a 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -823,10 +823,7 @@ impl User { let mut route = web::scope("/user").app_data(web::Data::new(state)); route = route - .service(web::resource("/signin").route(web::post().to(user_connect_account))) - .service(web::resource("/signup").route(web::post().to(user_connect_account))) - .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) - .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/signin").route(web::post().to(user_signin))) .service(web::resource("/change_password").route(web::post().to(change_password))) .service( web::resource("/data/merchant") @@ -841,7 +838,6 @@ impl User { ) .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) .service(web::resource("/user/list").route(web::get().to(get_user_details))) - // User Role APIs .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) @@ -855,6 +851,21 @@ impl User { .route(web::delete().to(delete_sample_data)), ) } + #[cfg(feature = "email")] + { + route = route + .service( + web::resource("/connect_account").route(web::post().to(user_connect_account)), + ) + .service( + web::resource("/signup_with_merchant_id") + .route(web::post().to(user_signup_with_merchant_id)), + ); + } + #[cfg(not(feature = "email"))] + { + route = route.service(web::resource("/signup").route(web::post().to(user_signup))) + } route } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6aa2bbad0b15..b32dbe3d4b6a 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -149,6 +149,8 @@ impl From for ApiIdentifier { | Flow::GsmRuleDelete => Self::Gsm, Flow::UserConnectAccount + | Flow::UserSignUp + | Flow::UserSignIn | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata @@ -159,7 +161,8 @@ impl From for ApiIdentifier { | Flow::GenerateSampleData | Flow::DeleteSampleData | Flow::UserMerchantAccountList - | Flow::GetUserDetails => Self::User, + | Flow::GetUserDetails + | Flow::UserSignUpWithMerchantId => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 97bd7054da9e..45fa0ba35c59 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -19,6 +19,65 @@ use crate::{ utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; +#[cfg(feature = "email")] +pub async fn user_signup_with_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUpWithMerchantId; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup_with_merchant_id(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUp; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signin( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignIn; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signin(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] pub async fn user_connect_account( state: web::Data, http_req: HttpRequest, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 082b29d80941..592195922493 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -26,7 +26,7 @@ use crate::{ db::StorageInterface, routes::AppState, services::{ - authentication::{AuthToken, UserFromToken}, + authentication::UserFromToken, authorization::{info, predefined_permissions}, }, types::transformers::ForeignFrom, @@ -215,6 +215,25 @@ impl NewUserOrganization { } } +impl TryFrom for NewUserOrganization { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let new_organization = api_org::OrganizationNew::new(Some( + UserCompanyName::new(value.company_name)?.get_secret(), + )); + let db_organization = ForeignFrom::foreign_from(new_organization); + Ok(Self(db_organization)) + } +} + +impl From for NewUserOrganization { + fn from(_value: user_api::SignUpRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + impl From for NewUserOrganization { fn from(_value: user_api::ConnectAccountRequest) -> Self { let new_organization = api_org::OrganizationNew::new(None); @@ -334,6 +353,24 @@ impl NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let merchant_id = MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))?; + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + impl TryFrom for NewUserMerchant { type Error = error_stack::Report; @@ -352,6 +389,21 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let company_name = Some(UserCompanyName::new(value.company_name.clone())?); + let merchant_id = MerchantId::new(value.company_name.clone())?; + let new_organization = NewUserOrganization::try_from(value)?; + + Ok(Self { + company_name, + merchant_id, + new_organization, + }) + } +} + impl TryFrom for NewUserMerchant { type Error = error_stack::Report; @@ -434,10 +486,23 @@ impl NewUser { .attach_printable("Error while inserting user") } + pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> { + if state + .store + .find_user_by_email(self.get_email().into_inner().expose().expose().as_str()) + .await + .is_ok() + { + return Err(UserErrors::UserExists).into_report(); + } + Ok(()) + } + pub async fn insert_user_and_merchant_in_db( &self, state: AppState, ) -> UserResult { + self.check_if_already_exists_in_db(state.clone()).await?; let db = state.store.as_ref(); let merchant_id = self.get_new_merchant().get_merchant_id(); self.new_merchant @@ -495,6 +560,46 @@ impl TryFrom for storage_user::UserNew { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let user_id = uuid::Uuid::new_v4().to_string(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + name, + email, + password, + user_id, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::try_from(value.email.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + impl TryFrom for NewUser { type Error = error_stack::Report; @@ -502,7 +607,7 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.email.clone().try_into()?; let name = UserName::try_from(value.email.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { @@ -582,41 +687,6 @@ impl UserFromStorage { self.0.email.clone() } - pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - let merchant_id = state - .store - .find_user_role_by_user_id(self.get_user_id()) - .await - .change_context(UserErrors::InternalServerError)? - .merchant_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await - } - - pub async fn get_jwt_auth_token_with_custom_merchant_id( - &self, - state: AppState, - merchant_id: String, - org_id: String, - ) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await - } - pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 696aa4090044..0403d9b453d0 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,11 +1,13 @@ -use diesel_models::enums::UserStatus; +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user_role::UserRole}; use error_stack::ResultExt; +use masking::Secret; use crate::{ core::errors::{UserErrors, UserResult}, routes::AppState, - services::authentication::UserFromToken, - types::domain::MerchantAccount, + services::authentication::{AuthToken, UserFromToken}, + types::domain::{MerchantAccount, UserFromStorage}, }; pub mod dashboard_metadata; @@ -68,3 +70,52 @@ pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserRe }) .collect()) } + +pub async fn generate_jwt_auth_token( + state: AppState, + user: &UserFromStorage, + user_role: &UserRole, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + user_role.merchant_id.clone(), + user_role.role_id.clone(), + &state.conf, + user_role.org_id.clone(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub async fn generate_jwt_auth_token_with_custom_merchant_id( + state: AppState, + user: &UserFromStorage, + user_role: &UserRole, + merchant_id: String, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + merchant_id, + user_role.role_id.clone(), + &state.conf, + user_role.org_id.to_owned(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub fn get_dashboard_entry_response( + user: UserFromStorage, + user_role: UserRole, + token: Secret, +) -> user_api::DashboardEntryResponse { + user_api::DashboardEntryResponse { + merchant_id: user_role.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: user_role.role_id, + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index f54a5a82baaf..606ae1f2f169 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -249,6 +249,12 @@ pub enum Flow { GsmRuleUpdate, /// Gsm Rule Delete flow GsmRuleDelete, + /// User Sign Up + UserSignUp, + /// User Sign Up + UserSignUpWithMerchantId, + /// User Sign In + UserSignIn, /// User connect account UserConnectAccount, /// Upsert Decision Manager Config From 57591f819c7994099e76cff1affc7bcf3e45a031 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:11:30 +0530 Subject: [PATCH 136/443] refactor: create separate struct for surcharge details response (#3027) --- crates/api_models/src/payment_methods.rs | 120 +------------ crates/api_models/src/payments.rs | 12 -- .../src/surcharge_decision_configs.rs | 10 +- crates/common_utils/src/types.rs | 15 +- crates/diesel_models/src/payment_intent.rs | 1 - .../surcharge_decision_configs.rs | 105 +++++++----- crates/router/src/core/payments.rs | 11 +- crates/router/src/core/payments/helpers.rs | 40 ++++- .../payments/operations/payment_create.rs | 8 +- .../payments/operations/payment_update.rs | 20 ++- crates/router/src/core/payments/types.rs | 158 +++++++++++++++++- crates/router/src/core/utils.rs | 22 ++- crates/router/src/types.rs | 8 +- crates/router/src/types/api.rs | 12 +- 14 files changed, 325 insertions(+), 217 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index dfb8e8999771..3343becaaae6 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use cards::CardNumber; use common_utils::{ - consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii, - types::Percentage, + consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, + crypto::OptionalEncryptableName, + pii, + types::{Percentage, Surcharge}, }; use serde::de; use utoipa::ToSchema; @@ -14,7 +16,7 @@ use crate::{ admin, customers::CustomerId, enums as api_enums, - payments::{self, BankCodeResponse, RequestSurchargeDetails}, + payments::{self, BankCodeResponse}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -337,117 +339,11 @@ pub struct SurchargeDetailsResponse { /// tax on surcharge value pub tax_on_surcharge: Option>, /// surcharge amount for this payment - pub surcharge_amount: i64, + pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment - pub tax_on_surcharge_amount: i64, + pub display_tax_on_surcharge_amount: f64, /// sum of original amount, - pub final_amount: i64, -} - -impl SurchargeDetailsResponse { - pub fn is_request_surcharge_matching( - &self, - request_surcharge_details: RequestSurchargeDetails, - ) -> bool { - request_surcharge_details.surcharge_amount == self.surcharge_amount - && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount - } - pub fn get_total_surcharge_amount(&self) -> i64 { - self.surcharge_amount + self.tax_on_surcharge_amount - } -} - -#[derive(Clone, Debug)] -pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetailsResponse, - >, - pub payment_attempt_id: String, -} - -impl SurchargeMetadata { - pub fn new(payment_attempt_id: String) -> Self { - Self { - surcharge_results: HashMap::new(), - payment_attempt_id, - } - } - pub fn is_empty_result(&self) -> bool { - self.surcharge_results.is_empty() - } - pub fn get_surcharge_results_size(&self) -> usize { - self.surcharge_results.len() - } - pub fn insert_surcharge_details( - &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - surcharge_details: SurchargeDetailsResponse, - ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); - } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetailsResponse> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) - } - pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { - format!("surcharge_metadata_{}", payment_attempt_id) - } - pub fn get_individual_surcharge_key_value_pairs( - &self, - ) -> Vec<(String, SurchargeDetailsResponse)> { - self.surcharge_results - .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); - (key, surcharge_details.to_owned()) - }) - .collect() - } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) - } - } -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] -#[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - /// Fixed Surcharge value - Fixed(i64), - /// Surcharge percentage - Rate(Percentage), + pub display_final_amount: f64, } /// Required fields info used while listing the payment_method_data diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 49f2781a18a0..eaf0937ef2a2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,7 +16,6 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, - payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -340,17 +339,6 @@ impl RequestSurchargeDetails { pub fn is_surcharge_zero(&self) -> bool { self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 } - pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { - let surcharge_amount = self.surcharge_amount; - let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); - SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(self.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, - } - } pub fn get_total_surcharge_amount(&self) -> i64 { self.surcharge_amount + self.tax_amount.unwrap_or(0) } diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs index 3ebf8f42744e..7ead27945584 100644 --- a/crates/api_models/src/surcharge_decision_configs.rs +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -7,21 +7,21 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub struct SurchargeDetails { - pub surcharge: Surcharge, +pub struct SurchargeDetailsOutput { + pub surcharge: SurchargeOutput, pub tax_on_surcharge: Option>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - Fixed(i64), +pub enum SurchargeOutput { + Fixed { amount: i64 }, Rate(Percentage), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct SurchargeDecisionConfigs { - pub surcharge_details: Option, + pub surcharge_details: Option, } impl EuclidDirFilter for SurchargeDecisionConfigs { const ALLOWED: &'static [DirKeyKind] = &[ diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 111f0f43c0f2..cf94f2fe26ce 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -2,7 +2,10 @@ use error_stack::{IntoReport, ResultExt}; use serde::{de::Visitor, Deserialize, Deserializer}; -use crate::errors::{CustomResult, PercentageError}; +use crate::{ + consts, + errors::{CustomResult, PercentageError}, +}; /// Represents Percentage Value between 0 and 100 both inclusive #[derive(Clone, Default, Debug, PartialEq, serde::Serialize)] @@ -136,3 +139,13 @@ impl<'de, const PRECISION: u8> Deserialize<'de> for Percentage { data.deserialize_map(PercentageVisitor:: {}) } } + +/// represents surcharge type and value +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(Percentage<{ consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH }>), +} diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 8d752466103e..40d8fd92caeb 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -106,7 +106,6 @@ pub struct PaymentIntentNew { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, - pub updated_by: String, pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 9a65ec76f2a5..38ae71754b87 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -1,12 +1,10 @@ use api_models::{ - payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata}, + payment_methods::SurchargeDetailsResponse, payments::Address, routing, - surcharge_decision_configs::{ - self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails, - }, + surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord}, }; -use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache, types as common_utils_types}; use error_stack::{self, IntoReport, ResultExt}; use euclid::{ backend, @@ -14,7 +12,11 @@ use euclid::{ }; use router_env::{instrument, tracing}; -use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage}; +use crate::{ + core::payments::{types, PaymentData}, + db::StorageInterface, + types::{storage as oss_storage, transformers::ForeignTryFrom}, +}; static CONF_CACHE: StaticCache = StaticCache::new(); use crate::{ core::{ @@ -55,10 +57,10 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( billing_address: Option
, response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], ) -> ConditionalConfigResult<( - SurchargeMetadata, + types::SurchargeMetadata, surcharge_decision_configs::MerchantSurchargeConfigs, )> { - let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { id } else { @@ -101,20 +103,27 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( Some(card_network_type.card_network.clone()); let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + // let surcharge_details = card_network_type.surcharge_details = surcharge_output .surcharge_details .map(|surcharge_details| { - get_surcharge_details_response(surcharge_details, payment_attempt).map( - |surcharge_details_response| { - surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), - surcharge_details_response.clone(), - ); - surcharge_details_response - }, - ) + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + Some(&card_network_type.card_network), + surcharge_details.clone(), + ); + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") }) .transpose()?; } @@ -124,17 +133,23 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_method_type_response.surcharge_details = surcharge_output .surcharge_details .map(|surcharge_details| { - get_surcharge_details_response(surcharge_details, payment_attempt).map( - |surcharge_details_response| { - surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, - surcharge_details_response.clone(), - ); - surcharge_details_response - }, - ) + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + None, + surcharge_details.clone(), + ); + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") }) .transpose()?; } @@ -148,12 +163,12 @@ pub async fn perform_surcharge_decision_management_for_session_flow( algorithm_ref: routing::RoutingAlgorithmRef, payment_data: &mut PaymentData, payment_method_type_list: &Vec, -) -> ConditionalConfigResult +) -> ConditionalConfigResult where O: Send + Clone, { let mut surcharge_metadata = - SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + types::SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { id } else { @@ -186,8 +201,10 @@ where let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details_response = - get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?; + let surcharge_details_response = get_surcharge_details_from_surcharge_output( + surcharge_details, + &payment_data.payment_attempt, + )?; surcharge_metadata.insert_surcharge_details( &payment_method_type.to_owned().into(), payment_method_type, @@ -199,13 +216,13 @@ where Ok(surcharge_metadata) } -fn get_surcharge_details_response( - surcharge_details: SurchargeDetails, +fn get_surcharge_details_from_surcharge_output( + surcharge_details: surcharge_decision_configs::SurchargeDetailsOutput, payment_attempt: &oss_storage::PaymentAttempt, -) -> ConditionalConfigResult { +) -> ConditionalConfigResult { let surcharge_amount = match surcharge_details.surcharge.clone() { - surcharge_decision_configs::Surcharge::Fixed(value) => value, - surcharge_decision_configs::Surcharge::Rate(percentage) => percentage + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => amount, + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => percentage .apply_and_ceil_result(payment_attempt.amount) .change_context(ConfigError::DslExecutionError) .attach_printable("Failed to Calculate surcharge amount by applying percentage")?, @@ -221,13 +238,13 @@ fn get_surcharge_details_response( }) .transpose()? .unwrap_or(0); - Ok(SurchargeDetailsResponse { + Ok(types::SurchargeDetails { surcharge: match surcharge_details.surcharge { - surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => { - payment_methods::Surcharge::Fixed(surcharge_amount) + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => { + common_utils_types::Surcharge::Fixed(amount) } - surcharge_decision_configs::Surcharge::Rate(percentage) => { - payment_methods::Surcharge::Rate(percentage) + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => { + common_utils_types::Surcharge::Rate(percentage) } }, tax_on_surcharge: surcharge_details.tax_on_surcharge, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 33afa29397e1..67b1dbcc98b2 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -15,10 +15,9 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI use api_models::{ self, enums, - payment_methods::{Surcharge, SurchargeDetailsResponse}, payments::{self, HeaderPayload}, }; -use common_utils::{ext_traits::AsyncExt, pii}; +use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; use error_stack::{IntoReport, ResultExt}; @@ -42,6 +41,7 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, + types::SurchargeDetails, }; use super::{ errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, @@ -475,8 +475,7 @@ where .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - surcharge_details - .get_surcharge_details_object(payment_data.payment_attempt.amount) + SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) }); payment_data.surcharge_details = surcharge_details; } @@ -509,7 +508,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetailsResponse { + SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -1882,7 +1881,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0cce91bebeeb..59b2b53c654d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use api_models::payments::{CardToken, GetPaymentMethodType}; +use api_models::payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -572,6 +572,7 @@ pub fn validate_merchant_id( pub fn validate_request_amount_and_amount_to_capture( op_amount: Option, op_amount_to_capture: Option, + surcharge_details: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { match (op_amount, op_amount_to_capture) { (None, _) => Ok(()), @@ -581,7 +582,11 @@ pub fn validate_request_amount_and_amount_to_capture( api::Amount::Value(amount_inner) => { // If both amount and amount to capture is present // then amount to be capture should be less than or equal to request amount - utils::when(!amount_to_capture.le(&amount_inner.get()), || { + let total_capturable_amount = amount_inner.get() + + surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .unwrap_or(0); + utils::when(!amount_to_capture.le(&total_capturable_amount), || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: format!( "amount_to_capture is greater than amount capture_amount: {amount_to_capture:?} request_amount: {amount:?}" @@ -603,13 +608,34 @@ pub fn validate_request_amount_and_amount_to_capture( /// if capture method = automatic, amount_to_capture(if provided) must be equal to amount #[instrument(skip_all)] -pub fn validate_amount_to_capture_in_create_call_request( +pub fn validate_amount_to_capture_and_capture_method( + payment_attempt: Option<&PaymentAttempt>, request: &api_models::payments::PaymentsRequest, ) -> CustomResult<(), errors::ApiErrorResponse> { - if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic { - let total_capturable_amount = request.get_total_capturable_amount(); - if let Some((amount_to_capture, total_capturable_amount)) = - request.amount_to_capture.zip(total_capturable_amount) + let capture_method = request + .capture_method + .or(payment_attempt + .map(|payment_attempt| payment_attempt.capture_method.unwrap_or_default())) + .unwrap_or_default(); + if capture_method == api_enums::CaptureMethod::Automatic { + let original_amount = request + .amount + .map(|amount| amount.into()) + .or(payment_attempt.map(|payment_attempt| payment_attempt.amount)); + let surcharge_amount = request + .surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .or_else(|| { + payment_attempt.map(|payment_attempt| { + payment_attempt.surcharge_amount.unwrap_or(0) + + payment_attempt.tax_amount.unwrap_or(0) + }) + }) + .unwrap_or(0); + let total_capturable_amount = + original_amount.map(|original_amount| original_amount + surcharge_amount); + if let Some((total_capturable_amount, amount_to_capture)) = + total_capturable_amount.zip(request.amount_to_capture) { utils::when(amount_to_capture != total_capturable_amount, || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index ac387076d1d1..bb7d0a931e1b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -286,8 +286,8 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(MandateData::from); - let surcharge_details = request.surcharge_details.map(|surcharge_details| { - surcharge_details.get_surcharge_details_object(payment_attempt.amount) + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { @@ -540,14 +540,14 @@ impl ValidateRequest &[ storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::PartiallyCaptured, storage_enums::IntentStatus::RequiresCapture, ], "update", @@ -134,6 +135,20 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + helpers::validate_amount_to_capture_and_capture_method(Some(&payment_attempt), request)?; + + helpers::validate_request_amount_and_amount_to_capture( + request.amount, + request.amount_to_capture, + request + .surcharge_details + .or(payment_attempt.get_surcharge_details()), + ) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "amount_to_capture".to_string(), + expected_format: "amount_to_capture lesser than or equal to amount".to_string(), + })?; + currency = request .currency .or(payment_attempt.currency) @@ -322,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) + payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { @@ -629,6 +644,7 @@ impl ValidateRequest>, + /// surcharge amount for this payment + pub surcharge_amount: i64, + /// tax on surcharge amount for this payment + pub tax_on_surcharge_amount: i64, + /// sum of original amount, + pub final_amount: i64, +} + +impl From<(&RequestSurchargeDetails, &PaymentAttempt)> for SurchargeDetails { + fn from( + (request_surcharge_details, payment_attempt): (&RequestSurchargeDetails, &PaymentAttempt), + ) -> Self { + let surcharge_amount = request_surcharge_details.surcharge_amount; + let tax_on_surcharge_amount = request_surcharge_details.tax_amount.unwrap_or(0); + Self { + surcharge: common_types::Surcharge::Fixed(request_surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + +impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsResponse { + type Error = TryFromIntError; + fn foreign_try_from( + (surcharge_details, payment_attempt): (&SurchargeDetails, &PaymentAttempt), + ) -> Result { + let currency = payment_attempt.currency.unwrap_or_default(); + let display_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.surcharge_amount)?; + let display_tax_on_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.tax_on_surcharge_amount)?; + let display_final_amount = + currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; + Ok(Self { + surcharge: surcharge_details.surcharge.clone(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + display_surcharge_amount, + display_tax_on_surcharge_amount, + display_final_amount, + }) + } +} + +impl SurchargeDetails { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + request_surcharge_details.surcharge_amount == self.surcharge_amount + && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount + } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_on_surcharge_amount + } +} + +#[derive(Clone, Debug)] +pub struct SurchargeMetadata { + surcharge_results: HashMap< + ( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), + SurchargeDetails, + >, + pub payment_attempt_id: String, +} + +impl SurchargeMetadata { + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + pub fn insert_surcharge_details( + &mut self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + surcharge_details: SurchargeDetails, + ) { + let key = ( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.insert(key, surcharge_details); + } + pub fn get_surcharge_details( + &self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> Option<&SurchargeDetails> { + let key = &( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.get(key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { + self.surcharge_results + .iter() + .map(|((pm, pmt, card_network), surcharge_details)| { + let key = + Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + (key, surcharge_details.to_owned()) + }) + .collect() + } + pub fn get_surcharge_details_redis_hashset_key( + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> String { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 670c25c814ed..6d82c44d803a 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,9 +1,6 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::{ - enums::{DisputeStage, DisputeStatus}, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, -}; +use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; @@ -17,7 +14,7 @@ use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, PaymentAddress}; +use super::payments::{helpers, types as payments_types, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1075,7 +1072,7 @@ pub fn get_flow_name() -> RouterResult { pub async fn persist_individual_surcharge_details_in_redis( state: &AppState, merchant_account: &domain::MerchantAccount, - surcharge_metadata: &SurchargeMetadata, + surcharge_metadata: &payments_types::SurchargeMetadata, ) -> RouterResult<()> { if !surcharge_metadata.is_empty_result() { let redis_conn = state @@ -1083,7 +1080,7 @@ pub async fn persist_individual_surcharge_details_in_redis( .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key( + let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( &surcharge_metadata.payment_attempt_id, ); @@ -1094,7 +1091,7 @@ pub async fn persist_individual_surcharge_details_in_redis( { value_list.push(( key, - Encode::::encode_to_string_of_json(&value) + Encode::::encode_to_string_of_json(&value) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to encode to string of json")?, )); @@ -1118,20 +1115,21 @@ pub async fn get_individual_surcharge_detail_from_redis( payment_method_type: &euclid_enums::PaymentMethodType, card_network: Option, payment_attempt_id: &str, -) -> CustomResult { +) -> CustomResult { let redis_conn = state .store .get_redis_conn() .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = SurchargeMetadata::get_surcharge_details_redis_hashset_key( + let redis_key = + payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( payment_method, payment_method_type, card_network.as_ref(), ); redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") .await } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c267a54cc57b..595f487e7079 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -30,7 +30,7 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::{PaymentData, RecurringMandatePaymentData}, + payments::{types, PaymentData, RecurringMandatePaymentData}, }, services, types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, @@ -379,7 +379,7 @@ pub struct PaymentsAuthorizeData { pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub customer_id: Option, pub request_incremental_authorization: bool, } @@ -441,7 +441,7 @@ pub struct PaymentsPreProcessingData { pub router_return_url: Option, pub webhook_url: Option, pub complete_authorize_url: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub browser_info: Option, pub connector_transaction_id: Option, } @@ -517,7 +517,7 @@ pub struct PaymentsSessionData { pub amount: i64, pub currency: storage_enums::Currency, pub country: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub order_details: Option>, } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 96bcaca3ed5d..ea2ea8b701da 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -19,7 +19,6 @@ pub mod webhooks; use std::{fmt::Debug, str::FromStr}; -use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ @@ -30,7 +29,10 @@ use super::ErrorResponse; use crate::{ configs::settings::Connectors, connector, consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::types as payments_types, + }, services::{request, ConnectorIntegration, ConnectorRedirectResponse, ConnectorValidation}, types::{self, api::enums as api_enums}, }; @@ -222,9 +224,9 @@ pub struct SessionConnectorData { /// Session Surcharge type pub enum SessionSurchargeDetails { /// Surcharge is calculated by hyperswitch - Calculated(SurchargeMetadata), + Calculated(payments_types::SurchargeMetadata), /// Surcharge is sent by merchant - PreDetermined(SurchargeDetailsResponse), + PreDetermined(payments_types::SurchargeDetails), } impl SessionSurchargeDetails { @@ -233,7 +235,7 @@ impl SessionSurchargeDetails { payment_method: &enums::PaymentMethod, payment_method_type: &enums::PaymentMethodType, card_network: Option<&enums::CardNetwork>, - ) -> Option { + ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata .get_surcharge_details(payment_method, payment_method_type, card_network) From 9274cefbdd29d2ac64baeea2fe504dff2472cb47 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:49:15 +0530 Subject: [PATCH 137/443] fix(pm_list): [Trustpay]Update dynamic fields for trustpay blik (#3042) --- crates/router/src/configs/defaults.rs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index db29e8180089..d529ae034a86 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4293,24 +4293,6 @@ impl Default for super::settings::RequiredFields { mandate: HashMap::new(), non_mandate: HashMap::new(), common: HashMap::from([ - ( - "billing.address.first_name".to_string(), - RequiredFieldInfo { - required_field: "billing.address.first_name".to_string(), - display_name: "billing_first_name".to_string(), - field_type: enums::FieldType::UserBillingName, - value: None, - } - ), - ( - "billing.address.last_name".to_string(), - RequiredFieldInfo { - required_field: "billing.address.last_name".to_string(), - display_name: "billing_last_name".to_string(), - field_type: enums::FieldType::UserBillingName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -4324,7 +4306,7 @@ impl Default for super::settings::RequiredFields { "billing.address.first_name".to_string(), RequiredFieldInfo { required_field: "billing.address.first_name".to_string(), - display_name: "card_holder_name".to_string(), + display_name: "billing_first_name".to_string(), field_type: enums::FieldType::UserBillingName, value: None, } @@ -4333,7 +4315,7 @@ impl Default for super::settings::RequiredFields { "billing.address.last_name".to_string(), RequiredFieldInfo { required_field: "billing.address.last_name".to_string(), - display_name: "card_holder_name".to_string(), + display_name: "billing_last_name".to_string(), field_type: enums::FieldType::UserBillingName, value: None, } From a0cfdd3fb12f04b603f65551eac985c31e08da85 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:34:51 +0530 Subject: [PATCH 138/443] feat(router): add payments incremental authorization api (#3038) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/payment.rs | 13 +- crates/api_models/src/payments.rs | 36 ++ crates/common_enums/src/enums.rs | 26 ++ crates/common_utils/src/request.rs | 1 + crates/data_models/src/payments.rs | 1 + .../src/payments/payment_attempt.rs | 4 + .../src/payments/payment_intent.rs | 18 + crates/diesel_models/src/authorization.rs | 78 +++++ crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/payment_attempt.rs | 12 + crates/diesel_models/src/payment_intent.rs | 21 ++ crates/diesel_models/src/query.rs | 1 + .../diesel_models/src/query/authorization.rs | 79 +++++ crates/diesel_models/src/schema.rs | 27 ++ crates/router/src/connector/cybersource.rs | 116 ++++++- .../src/connector/cybersource/transformers.rs | 152 +++++++- crates/router/src/core/payments.rs | 19 +- crates/router/src/core/payments/flows.rs | 80 +++++ .../flows/incremental_authorization_flow.rs | 118 +++++++ crates/router/src/core/payments/helpers.rs | 17 + crates/router/src/core/payments/operations.rs | 2 + .../payments/operations/payment_approve.rs | 2 + .../payments/operations/payment_cancel.rs | 2 + .../payments/operations/payment_capture.rs | 2 + .../operations/payment_complete_authorize.rs | 2 + .../payments/operations/payment_confirm.rs | 2 + .../payments/operations/payment_create.rs | 3 + .../payments/operations/payment_reject.rs | 2 + .../payments/operations/payment_response.rs | 139 +++++++- .../payments/operations/payment_session.rs | 2 + .../core/payments/operations/payment_start.rs | 2 + .../payments/operations/payment_status.rs | 16 + .../payments/operations/payment_update.rs | 2 + .../payments_incremental_authorization.rs | 328 ++++++++++++++++++ .../router/src/core/payments/transformers.rs | 62 +++- crates/router/src/db.rs | 2 + crates/router/src/db/authorization.rs | 104 ++++++ crates/router/src/db/kafka_store.rs | 36 ++ crates/router/src/openapi.rs | 2 + crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/payments.rs | 77 ++++ crates/router/src/services/api.rs | 4 + crates/router/src/types.rs | 26 ++ crates/router/src/types/api/payments.rs | 24 +- crates/router/src/types/storage.rs | 3 +- .../router/src/types/storage/authorization.rs | 1 + crates/router/src/types/transformers.rs | 13 + crates/router/src/utils/user/sample_data.rs | 1 + crates/router/tests/connectors/utils.rs | 2 + crates/router_derive/src/macros/operation.rs | 12 +- crates/router_env/src/logger/types.rs | 2 + .../src/mock_db/payment_intent.rs | 1 + .../src/payments/payment_attempt.rs | 14 + .../src/payments/payment_intent.rs | 13 + .../down.sql | 2 + .../up.sql | 16 + .../down.sql | 2 + .../up.sql | 2 + openapi/openapi_spec.json | 61 ++++ 60 files changed, 1792 insertions(+), 22 deletions(-) create mode 100644 crates/diesel_models/src/authorization.rs create mode 100644 crates/diesel_models/src/query/authorization.rs create mode 100644 crates/router/src/core/payments/flows/incremental_authorization_flow.rs create mode 100644 crates/router/src/core/payments/operations/payments_incremental_authorization.rs create mode 100644 crates/router/src/db/authorization.rs create mode 100644 crates/router/src/types/storage/authorization.rs create mode 100644 migrations/2023-11-30-170902_add-authorizations-table/down.sql create mode 100644 migrations/2023-11-30-170902_add-authorizations-table/up.sql create mode 100644 migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql create mode 100644 migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 2f3336fc2777..f718dc1ca4dd 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -8,8 +8,9 @@ use crate::{ payments::{ PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, - PaymentsCaptureRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + PaymentsCaptureRequest, PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, + PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, + RedirectionResponse, }, }; impl ApiEventMetric for PaymentsRetrieveRequest { @@ -149,3 +150,11 @@ impl ApiEventMetric for PaymentListResponseV2 { } impl ApiEventMetric for RedirectionResponse {} + +impl ApiEventMetric for PaymentsIncrementalAuthorizationRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index eaf0937ef2a2..0da6822f1501 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2209,6 +2209,12 @@ pub struct PaymentsResponse { /// If true incremental authorization can be performed on this payment pub incremental_authorization_allowed: Option, + + /// Total number of authorizations happened in an incremental_authorization payment + pub authorization_count: Option, + + /// List of incremental authorizations happened to the payment + pub incremental_authorizations: Option>, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -2277,6 +2283,24 @@ pub struct PaymentListResponse { // The list of payments response objects pub data: Vec, } + +#[derive(Setter, Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct IncrementalAuthorizationResponse { + /// The unique identifier of authorization + pub authorization_id: String, + /// Amount the authorization has been made for + pub amount: i64, + #[schema(value_type= AuthorizationStatus)] + /// The status of the authorization + pub status: common_enums::AuthorizationStatus, + /// Error code sent by the connector for authorization + pub error_code: Option, + /// Error message sent by the connector for authorization + pub error_message: Option, + /// Previously authorized amount for the payment + pub previously_authorized_amount: i64, +} + #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentListResponseV2 { /// The number of payments included in the list for given constraints @@ -2985,6 +3009,18 @@ pub struct PaymentsCancelRequest { pub merchant_connector_details: Option, } +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct PaymentsIncrementalAuthorizationRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, + /// The total amount including previously authorized amount and additional amount + #[schema(value_type = i64, example = 6540)] + pub amount: i64, + /// Reason for incremental authorization + pub reason: Option, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentsApproveRequest { /// The identifier for the payment diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 8da4a2da54cc..7615c0cc8804 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -246,6 +246,32 @@ pub enum CaptureStatus { Failed, } +#[derive( + Default, + Clone, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthorizationStatus { + Success, + Failure, + // Processing state is before calling connector + #[default] + Processing, + // Requires merchant action + Unresolved, +} + #[derive( Clone, Copy, diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index 64bce8649d97..d6d9281a4a05 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -17,6 +17,7 @@ pub enum Method { Post, Put, Delete, + Patch, } #[derive(Deserialize, Serialize, Debug)] diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index af2076bfa10d..7a4787fcf0a0 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -52,4 +52,5 @@ pub struct PaymentIntent { pub surcharge_applicable: Option, pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 44aa48b142ad..f7b849f1d4e1 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -359,6 +359,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } impl ForeignIDRef for PaymentAttempt { diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index d8f927a4e2c5..d7edcfdf1791 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -109,6 +109,7 @@ pub struct PaymentIntentNew { pub surcharge_applicable: Option, pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -186,6 +187,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: bool, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default)] @@ -218,6 +225,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } impl From for PaymentIntentUpdateInternal { @@ -381,6 +389,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/authorization.rs b/crates/diesel_models/src/authorization.rs new file mode 100644 index 000000000000..64fd1c65187d --- /dev/null +++ b/crates/diesel_models/src/authorization.rs @@ -0,0 +1,78 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::incremental_authorization}; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Hash)] +#[diesel(table_name = incremental_authorization)] +#[diesel(primary_key(authorization_id, merchant_id))] +pub struct Authorization { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationNew { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthorizationUpdate { + StatusUpdate { + status: storage_enums::AuthorizationStatus, + error_code: Option, + error_message: Option, + connector_authorization_id: Option, + }, +} + +#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationUpdateInternal { + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: Option, + pub connector_authorization_id: Option, +} + +impl From for AuthorizationUpdateInternal { + fn from(authorization_child_update: AuthorizationUpdate) -> Self { + let now = Some(common_utils::date_time::now()); + match authorization_child_update { + AuthorizationUpdate::StatusUpdate { + status, + error_code, + error_message, + connector_authorization_id, + } => Self { + status: Some(status), + error_code, + error_message, + connector_authorization_id, + modified_at: now, + }, + } + } +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 781099662a50..fa32fb84a15d 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; pub mod customers; pub mod dispute; pub mod encryption; diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 216801fa8fb1..b1e8e144a9e3 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -269,6 +269,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -679,6 +683,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, + PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self { + amount: Some(amount), + amount_capturable: Some(amount_capturable), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 40d8fd92caeb..1bd5c73a96ca 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -54,6 +54,7 @@ pub struct PaymentIntent { pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive( @@ -110,6 +111,7 @@ pub struct PaymentIntentNew { pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -187,6 +189,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -220,6 +228,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } impl PaymentIntentUpdate { @@ -251,6 +260,7 @@ impl PaymentIntentUpdate { updated_by, surcharge_applicable, incremental_authorization_allowed, + authorization_count, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -282,6 +292,7 @@ impl PaymentIntentUpdate { surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), incremental_authorization_allowed, + authorization_count, ..source } } @@ -448,6 +459,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index b0537d0a287b..3a3dee47a854 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -5,6 +5,7 @@ mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; pub mod customers; pub mod dashboard_metadata; pub mod dispute; diff --git a/crates/diesel_models/src/query/authorization.rs b/crates/diesel_models/src/query/authorization.rs new file mode 100644 index 000000000000..dc9515bda55e --- /dev/null +++ b/crates/diesel_models/src/query/authorization.rs @@ -0,0 +1,79 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + authorization::{ + Authorization, AuthorizationNew, AuthorizationUpdate, AuthorizationUpdateInternal, + }, + errors, + schema::incremental_authorization::dsl, + PgPooledConn, StorageResult, +}; + +impl AuthorizationNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Authorization { + #[instrument(skip(conn))] + pub async fn update_by_merchant_id_authorization_id( + conn: &PgPooledConn, + merchant_id: String, + authorization_id: String, + authorization_update: AuthorizationUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + AuthorizationUpdateInternal::from(authorization_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NotFound => Err(error.attach_printable( + "Authorization with the given Authorization ID does not exist", + )), + errors::DatabaseError::NoFieldsToUpdate => { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + ) + .await + } + _ => Err(error), + }, + result => result, + } + } + + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_payment_id( + conn: &PgPooledConn, + merchant_id: &str, + payment_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payment_id.eq(payment_id.to_owned())), + None, + None, + Some(dsl::created_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 13b001ecc6d1..9baf613d9233 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -362,6 +362,31 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + incremental_authorization (authorization_id, merchant_id) { + #[max_length = 64] + authorization_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + payment_id -> Varchar, + amount -> Int8, + created_at -> Timestamp, + modified_at -> Timestamp, + #[max_length = 64] + status -> Varchar, + #[max_length = 255] + error_code -> Nullable, + error_message -> Nullable, + #[max_length = 64] + connector_authorization_id -> Nullable, + previously_authorized_amount -> Int8, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -680,6 +705,7 @@ diesel::table! { surcharge_applicable -> Nullable, request_incremental_authorization -> RequestIncrementalAuthorization, incremental_authorization_allowed -> Nullable, + authorization_count -> Nullable, } } @@ -997,6 +1023,7 @@ diesel::allow_tables_to_appear_in_same_query!( file_metadata, fraud_check, gateway_status_map, + incremental_authorization, locker_mock_up, mandate, merchant_account, diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 1de107af086d..631b2f8c97ed 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -217,7 +217,10 @@ where ("Host".to_string(), host.to_string().into()), ("Signature".to_string(), signature.into_masked()), ]; - if matches!(http_method, services::Method::Post | services::Method::Put) { + if matches!( + http_method, + services::Method::Post | services::Method::Put | services::Method::Patch + ) { headers.push(( "Digest".to_string(), format!("SHA-256={sha256}").into_masked(), @@ -232,6 +235,7 @@ impl api::PaymentAuthorize for Cybersource {} impl api::PaymentSync for Cybersource {} impl api::PaymentVoid for Cybersource {} impl api::PaymentCapture for Cybersource {} +impl api::PaymentIncrementalAuthorization for Cybersource {} impl api::MandateSetup for Cybersource {} impl api::ConnectorAccessToken for Cybersource {} impl api::PaymentToken for Cybersource {} @@ -872,6 +876,116 @@ impl ConnectorIntegration for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + 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::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.additional_amount, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsIncrementalAuthorizationRequest::try_from( + &connector_router_data, + )?; + let cybersource_payments_incremental_authorization_request = + types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(cybersource_payments_incremental_authorization_request)) + } + fn build_request( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Patch) + .url(&types::IncrementalAuthorizationType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::IncrementalAuthorizationType::get_headers( + self, req, connectors, + )?) + .body(types::IncrementalAuthorizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::PaymentsIncrementalAuthorizationRouterData, + res: types::Response, + ) -> CustomResult< + types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > { + let response: cybersource::CybersourcePaymentsIncrementalAuthorizationResponse = res + .response + .parse_struct("Cybersource PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + true, + )) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + #[async_trait::async_trait] impl api::IncomingWebhook for Cybersource { fn get_webhook_object_reference_id( diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 953f82c76a83..d3f542d2013a 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -77,9 +77,11 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { - initiator_type: CybersourcePaymentInitiatorTypes::Customer, - credential_stored_on_file: true, + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, }, + merchant_intitiated_transaction: None, }), ); @@ -158,14 +160,22 @@ pub enum CybersourceActionsTokenType { #[serde(rename_all = "camelCase")] pub struct CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator, + merchant_intitiated_transaction: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantInitiatedTransaction { + reason: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentInitiator { #[serde(rename = "type")] - initiator_type: CybersourcePaymentInitiatorTypes, - credential_stored_on_file: bool, + initiator_type: Option, + credential_stored_on_file: Option, + stored_credential_used: Option, } #[derive(Debug, Serialize)] @@ -229,6 +239,12 @@ pub struct OrderInformationWithBill { bill_to: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformation { @@ -242,6 +258,13 @@ pub struct Amount { currency: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalAmount { + additional_amount: String, + currency: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { @@ -305,9 +328,11 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { - initiator_type: CybersourcePaymentInitiatorTypes::Customer, - credential_stored_on_file: true, + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, }, + merchant_intitiated_transaction: None, }), ) } else { @@ -390,6 +415,13 @@ pub struct CybersourcePaymentsCaptureRequest { order_information: OrderInformationWithBill, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationIncrementalAuthorization, +} + impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> for CybersourcePaymentsCaptureRequest { @@ -420,6 +452,41 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> } } +impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>> + for CybersourcePaymentsIncrementalAuthorizationRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>, + ) -> Result { + Ok(Self { + processing_information: ProcessingInformation { + action_list: None, + action_token_types: None, + authorization_options: Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: None, + credential_stored_on_file: None, + stored_credential_used: Some(true), + }, + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: "5".to_owned(), + }), + }), + commerce_indicator: CybersourceCommerceIndicator::Internet, + capture: None, + capture_options: None, + }, + order_information: OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount { + additional_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), + }, + }, + }) + } +} + pub struct CybersourceAuthType { pub(super) api_key: Secret, pub(super) merchant_account: Secret, @@ -461,6 +528,14 @@ pub enum CybersourcePaymentStatus { Processing, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceIncrementalAuthorizationStatus { + Authorized, + Declined, + AuthorizedPendingReview, +} + impl From for enums::AttemptStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { @@ -477,6 +552,16 @@ impl From for enums::AttemptStatus { } } +impl From for common_enums::AuthorizationStatus { + fn from(item: CybersourceIncrementalAuthorizationStatus) -> Self { + match item { + CybersourceIncrementalAuthorizationStatus::Authorized + | CybersourceIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Success, + CybersourceIncrementalAuthorizationStatus::Declined => Self::Failure, + } + } +} + impl From for enums::RefundStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { @@ -499,6 +584,13 @@ pub struct CybersourcePaymentsResponse { token_information: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationResponse { + status: CybersourceIncrementalAuthorizationStatus, + error_information: Option, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceSetupMandatesResponse { @@ -654,6 +746,54 @@ impl } } +impl + TryFrom<( + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + )> for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + data: ( + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + ), + ) -> Result { + let item = data.0; + Ok(Self { + response: match item.response.error_information { + Some(error) => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus::Failure, + error_code: Some(error.reason), + error_message: Some(error.message), + connector_authorization_id: None, + }, + ), + _ => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: item.response.status.into(), + error_code: None, + error_message: None, + connector_authorization_id: None, + }, + ), + }, + ..item.data + }) + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceTransactionResponse { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 67b1dbcc98b2..8bc251f9c3d3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -32,8 +32,9 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, - PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, + PaymentIncrementalAuthorization, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, + PaymentUpdate, }; use self::{ conditional_configs::perform_decision_management, @@ -1884,6 +1885,16 @@ where pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, + pub incremental_authorization_details: Option, + pub authorizations: Vec, +} + +#[derive(Debug, Default, Clone)] +pub struct IncrementalAuthorizationDetails { + pub additional_amount: i64, + pub total_amount: i64, + pub reason: Option, + pub authorization_id: Option, } #[derive(Debug, Default, Clone)] @@ -1984,6 +1995,10 @@ pub fn should_call_connector( "CompleteAuthorize" => true, "PaymentApprove" => true, "PaymentSession" => true, + "PaymentIncrementalAuthorization" => matches!( + payment_data.payment_intent.status, + storage_enums::IntentStatus::RequiresCapture + ), _ => false, } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 9be6f5905b8b..94b8bc1ff5d4 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -3,6 +3,7 @@ pub mod authorize_flow; pub mod cancel_flow; pub mod capture_flow; pub mod complete_authorize_flow; +pub mod incremental_authorization_flow; pub mod psync_flow; pub mod reject_flow; pub mod session_flow; @@ -1577,3 +1578,82 @@ default_imp_for_reject!( connector::Worldpay, connector::Zen ); + +macro_rules! default_imp_for_incremental_authorization { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentIncrementalAuthorization for $path::$connector {} + impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentIncrementalAuthorization for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_incremental_authorization!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); diff --git a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs new file mode 100644 index 000000000000..387916bab7c9 --- /dev/null +++ b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs @@ -0,0 +1,118 @@ +use async_trait::async_trait; + +use super::ConstructFlowSpecificData; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + payments::{self, access_token, helpers, transformers, Feature, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult { + Box::pin(transformers::construct_payment_router_data::< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + )) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > +{ + async fn decide_flows<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + _customer: &Option, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, + connector_request: Option, + _key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &self, + call_connector_action, + connector_request, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } + + async fn build_flow_specific_connector_request( + &mut self, + state: &AppState, + connector: &api::ConnectorData, + call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + let request = match call_connector_action { + payments::CallConnectorAction::Trigger => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + connector_integration + .build_request(self, &state.conf.connectors) + .to_payment_failed_response()? + } + _ => None, + }; + + Ok((request, true)) + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 59b2b53c654d..3e01a7b193d0 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2417,6 +2417,20 @@ pub async fn get_merchant_fullfillment_time( } } +pub(crate) fn validate_payment_status_against_allowed_statuses( + intent_status: &storage_enums::IntentStatus, + allowed_statuses: &[storage_enums::IntentStatus], + action: &'static str, +) -> Result<(), errors::ApiErrorResponse> { + fp_utils::when(!allowed_statuses.contains(intent_status), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "You cannot {action} this payment because it has status {intent_status}", + ), + }) + }) +} + pub(crate) fn validate_payment_status_against_not_allowed_statuses( intent_status: &storage_enums::IntentStatus, not_allowed_statuses: &[storage_enums::IntentStatus], @@ -2612,6 +2626,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(900); @@ -2665,6 +2680,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2718,6 +2734,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 809c9e925de0..93db8f03ff5c 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -10,6 +10,7 @@ pub mod payment_session; pub mod payment_start; pub mod payment_status; pub mod payment_update; +pub mod payments_incremental_authorization; use api_models::enums::FrmSuggestion; use async_trait::async_trait; @@ -22,6 +23,7 @@ pub use self::{ payment_create::PaymentCreate, payment_reject::PaymentReject, payment_response::PaymentResponse, payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, + payments_incremental_authorization::PaymentIncrementalAuthorization, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index f51d7a93ee5e..fee0326df03c 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -251,6 +251,8 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index ae7810971896..7c8fbcc34979 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -171,6 +171,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 5b89cfdbcf0b..65b91f0401cf 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -215,6 +215,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 8b264edbb3d1..17b71172a349 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -245,6 +245,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index d718db79a6d0..91dd5be40f64 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -479,6 +479,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index bb7d0a931e1b..f3cd726a17c7 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -323,6 +323,8 @@ impl surcharge_details, frm_message: None, payment_link_data, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { @@ -757,6 +759,7 @@ impl PaymentCreate { updated_by: merchant_account.storage_scheme.to_string(), request_incremental_authorization, incremental_authorization_allowed: None, + authorization_count: None, }) } diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index ae606187a0a1..03bf6dd46b60 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -158,6 +158,8 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 9781ad651ee2..f92487d74a7b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use async_trait::async_trait; +use common_enums::AuthorizationStatus; use data_models::payments::payment_attempt::PaymentAttempt; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; @@ -36,7 +37,7 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( operations = "post_update_tracker", - flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data" + flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data,incremental_authorization_data" )] pub struct PaymentResponse; @@ -76,6 +77,138 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData } } +#[async_trait] +impl PostUpdateTracker, types::PaymentsIncrementalAuthorizationData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &'b AppState, + _payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData< + F, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + let incremental_authorization_details = payment_data + .incremental_authorization_details + .clone() + .ok_or_else(|| { + report!(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing incremental_authorization_details in payment_data") + })?; + // Update payment_intent and payment_attempt 'amount' if incremental_authorization is successful + let (option_payment_attempt_update, option_payment_intent_update) = + match router_data.response.clone() { + Err(_) => (None, None), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + .. + }) => { + if status == AuthorizationStatus::Success { + (Some( + storage::PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + amount_capturable: incremental_authorization_details.total_amount, + }, + ), Some( + storage::PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + }, + )) + } else { + (None, None) + } + } + _ => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow")?, + }; + //payment_attempt update + if let Some(payment_attempt_update) = option_payment_attempt_update { + payment_data.payment_attempt = db + .store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // payment_intent update + if let Some(payment_intent_update) = option_payment_intent_update { + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + payment_intent_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // Update the status of authorization record + let authorization_update = match &router_data.response { + Err(res) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: AuthorizationStatus::Failure, + error_code: Some(res.code.clone()), + error_message: Some(res.message.clone()), + connector_authorization_id: None, + }), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + error_code, + error_message, + connector_authorization_id, + }) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: status.clone(), + error_code: error_code.clone(), + error_message: error_message.clone(), + connector_authorization_id: connector_authorization_id.clone(), + }), + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow"), + }?; + let authorization_id = incremental_authorization_details + .authorization_id + .clone() + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing authorization_id in incremental_authorization_details in payment_data", + ), + )?; + db.store + .update_authorization_by_merchant_id_authorization_id( + router_data.merchant_id.clone(), + authorization_id, + authorization_update, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while updating authorization")?; + //Fetch all the authorizations of the payment and send in incremental authorization response + let authorizations = db + .store + .find_all_authorizations_by_merchant_id_payment_id( + &router_data.merchant_id, + &payment_data.payment_intent.payment_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while retrieving authorizations")?; + payment_data.authorizations = authorizations; + Ok(payment_data) + } +} + #[async_trait] impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( @@ -544,6 +677,7 @@ async fn payment_response_update_tracker( types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => (None, None), types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { @@ -637,6 +771,7 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.status, ), updated_by: storage_scheme.to_string(), + // make this false only if initial payment fails, if incremental authorization call fails don't make it false incremental_authorization_allowed: Some(false), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 6097a5e430ce..572bc710b963 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -195,6 +195,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 3a4ae2c2e0de..887edd030d13 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -169,6 +169,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d0cd4b32d3c2..0320cf50663e 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -314,6 +314,20 @@ async fn get_tracker_for_sync< ) })?; + let authorizations = db + .find_all_authorizations_by_merchant_id_payment_id( + &merchant_account.merchant_id, + &payment_id_str, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable_lazy(|| { + format!( + "Failed while getting authorizations list for, payment_id: {}, merchant_id: {}", + &payment_id_str, merchant_account.merchant_id + ) + })?; + let disputes = db .find_disputes_by_merchant_id_payment_id(&merchant_account.merchant_id, &payment_id_str) .await @@ -407,6 +421,8 @@ async fn get_tracker_for_sync< payment_link_data: None, surcharge_details: None, frm_message: frm_response.ok(), + incremental_authorization_details: None, + authorizations, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 68f064c539d1..e1c373171682 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -373,6 +373,8 @@ impl surcharge_details, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs new file mode 100644 index 000000000000..7346c46df120 --- /dev/null +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -0,0 +1,328 @@ +use std::marker::PhantomData; + +use api_models::{enums::FrmSuggestion, payments::PaymentsIncrementalAuthorizationRequest}; +use async_trait::async_trait; +use common_utils::errors::CustomResult; +use diesel_models::authorization::AuthorizationNew; +use error_stack::{report, IntoReport, ResultExt}; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payment_methods::PaymentMethodRetrieve, + payments::{ + self, helpers, operations, CustomerDetails, IncrementalAuthorizationDetails, + PaymentAddress, + }, + }, + routes::{app::StorageInterface, AppState}, + services, + types::{ + api::{self, PaymentIdTypeExt}, + domain, + storage::{self, enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] +#[operation(operations = "all", flow = "incremental_authorization")] +pub struct PaymentIncrementalAuthorization; + +#[async_trait] +impl + GetTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_id: &api::PaymentIdType, + request: &PaymentsIncrementalAuthorizationRequest, + _mandate_type: Option, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + ) -> RouterResult< + operations::GetTrackerResponse<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + > { + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let storage_scheme = merchant_account.storage_scheme; + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_allowed_statuses( + &payment_intent.status, + &[enums::IntentStatus::RequiresCapture], + "increment authorization", + )?; + + if payment_intent.incremental_authorization_allowed != Some(true) { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: + "You cannot increment authorization this payment because it is not allowed for incremental_authorization".to_owned(), + })? + } + + if request.amount < payment_intent.amount { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than original authorized amount".to_owned(), + })? + } + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_intent.payment_id.as_str(), + merchant_id, + attempt_id.clone().as_str(), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let currency = payment_attempt.currency.get_required_value("currency")?; + let amount = payment_attempt.amount; + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount: amount.into(), + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + billing: None, + shipping: None, + }, + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + incremental_authorization_details: Some(IncrementalAuthorizationDetails { + additional_amount: request.amount - amount, + total_amount: request.amount, + reason: request.reason.clone(), + authorization_id: None, + }), + authorizations: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl + UpdateTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + db: &'b AppState, + mut payment_data: payments::PaymentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + _mechant_key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: api::HeaderPayload, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + payments::PaymentData, + )> + where + F: 'b + Send, + { + let new_authorization_count = payment_data + .payment_intent + .authorization_count + .map(|count| count + 1) + .unwrap_or(1); + // Create new authorization record + let authorization_new = AuthorizationNew { + authorization_id: format!( + "{}_{}", + common_utils::generate_id_with_default_len("auth"), + new_authorization_count + ), + merchant_id: payment_data.payment_intent.merchant_id.clone(), + payment_id: payment_data.payment_intent.payment_id.clone(), + amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + status: common_enums::AuthorizationStatus::Processing, + error_code: None, + error_message: None, + connector_authorization_id: None, + previously_authorized_amount: payment_data.payment_intent.amount, + }; + let authorization = db + .store + .insert_authorization(authorization_new.clone()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: format!( + "Authorization with authorization_id {} already exists", + authorization_new.authorization_id + ), + }) + .attach_printable("failed while inserting new authorization")?; + // Update authorization_count in payment_intent + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count: new_authorization_count, + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update authorization_count in Payment Intent")?; + match &payment_data.incremental_authorization_details { + Some(details) => { + payment_data.incremental_authorization_details = + Some(IncrementalAuthorizationDetails { + authorization_id: Some(authorization.authorization_id), + ..details.clone() + }); + } + None => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("missing incremental_authorization_details in payment_data")?, + } + Ok((Box::new(self), payment_data)) + } +} + +impl + ValidateRequest + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &PaymentsIncrementalAuthorizationRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + operations::ValidateResult<'a>, + )> { + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(request.payment_id.to_owned()), + mandate_type: None, + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }, + )) + } +} + +#[async_trait] +impl + Domain for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + _db: &dyn StorageInterface, + _payment_data: &mut payments::PaymentData, + _request: Option, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + ), + errors::StorageError, + > { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a AppState, + _payment_data: &mut payments::PaymentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> RouterResult<( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + )> { + Ok((Box::new(self), None)) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + state: &AppState, + _request: &PaymentsIncrementalAuthorizationRequest, + _payment_intent: &storage::PaymentIntent, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + helpers::get_connector_default(state, None).await + } +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 51e139c97988..bd6d03e5625a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -4,7 +4,7 @@ use api_models::payments::{FrmMessage, RequestSurchargeDetails}; use common_enums::RequestIncrementalAuthorization; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use router_env::{instrument, tracing}; use super::{flows::Feature, PaymentData}; @@ -392,6 +392,18 @@ where ) }; + let incremental_authorizations_response = if payment_data.authorizations.is_empty() { + None + } else { + Some( + payment_data + .authorizations + .into_iter() + .map(ForeignInto::foreign_into) + .collect(), + ) + }; + let attempts_response = payment_data.attempts.map(|attempts| { attempts .into_iter() @@ -692,6 +704,8 @@ where .set_incremental_authorization_allowed( payment_intent.incremental_authorization_allowed, ) + .set_authorization_count(payment_intent.authorization_count) + .set_incremental_authorizations(incremental_authorizations_response) .to_owned(), headers, )) @@ -755,6 +769,8 @@ where unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, + authorization_count: payment_intent.authorization_count, + incremental_authorizations: incremental_authorizations_response, ..Default::default() }, headers, @@ -1078,6 +1094,50 @@ impl TryFrom> for types::PaymentsSyncData } } +impl TryFrom> + for types::PaymentsIncrementalAuthorizationData +{ + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + Ok(Self { + total_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + additional_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.additional_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + reason: payment_data + .incremental_authorization_details + .and_then(|details| details.reason), + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + }) + } +} + impl api::ConnectorTransactionId for Helcim { fn connector_transaction_id( &self, diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 6558cc6ace50..0cd4cb218810 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod authorization; pub mod business_profile; pub mod cache; pub mod capture; @@ -100,6 +101,7 @@ pub trait StorageInterface: + gsm::GsmInterface + user::UserInterface + user_role::UserRoleInterface + + authorization::AuthorizationInterface + user::sample_data::BatchSampleDataInterface + 'static { diff --git a/crates/router/src/db/authorization.rs b/crates/router/src/db/authorization.rs new file mode 100644 index 000000000000..f24daaf718ad --- /dev/null +++ b/crates/router/src/db/authorization.rs @@ -0,0 +1,104 @@ +use error_stack::IntoReport; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait AuthorizationInterface { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult; + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl AuthorizationInterface for Store { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + authorization + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Authorization::find_by_merchant_id_payment_id(&conn, merchant_id, payment_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Authorization::update_by_merchant_id_authorization_id( + &conn, + merchant_id, + authorization_id, + authorization, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl AuthorizationInterface for MockDb { + async fn insert_authorization( + &self, + _authorization: storage::AuthorizationNew, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + _merchant_id: &str, + _payment_id: &str, + ) -> CustomResult, errors::StorageError> { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + _merchant_id: String, + _authorization_id: String, + _authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 32548e36b6fb..db94c1bcbca9 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -32,6 +32,7 @@ use crate::{ db::{ address::AddressInterface, api_keys::ApiKeyInterface, + authorization::AuthorizationInterface, business_profile::BusinessProfileInterface, capture::CaptureInterface, cards_info::CardsInfoInterface, @@ -2095,3 +2096,38 @@ impl BatchSampleDataInterface for KafkaStore { Ok(refunds_list) } } + +#[async_trait::async_trait] +impl AuthorizationInterface for KafkaStore { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + self.diesel_store.insert_authorization(authorization).await + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_authorizations_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_authorization_by_merchant_id_authorization_id( + merchant_id, + authorization_id, + authorization, + ) + .await + } +} diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index cfb0268a9f80..82c98304c62b 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -175,6 +175,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::CaptureStatus, api_models::enums::ReconStatus, api_models::enums::ConnectorStatus, + api_models::enums::AuthorizationStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, @@ -315,6 +316,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::RequestSurchargeDetails, api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, + api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, api_models::refunds::RefundListRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 88806b565d3a..5d14d1219d32 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -343,6 +343,9 @@ impl Payments { web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}") .route(web::get().to(payments_complete_authorize)) .route(web::post().to(payments_complete_authorize)), + ) + .service( + web::resource("/{payment_id}/incremental_authorization").route(web::post().to(payments_incremental_authorization)), ); } route diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b32dbe3d4b6a..3592506f522b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -96,7 +96,8 @@ impl From for ApiIdentifier { | Flow::PaymentsSessionToken | Flow::PaymentsStart | Flow::PaymentsList - | Flow::PaymentsRedirect => Self::Payments, + | Flow::PaymentsRedirect + | Flow::PaymentsIncrementalAuthorization => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 979b15a3d7f2..e424e93c78ed 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -986,6 +986,67 @@ where } } +/// Payments - Incremental Authorization +/// +/// Authorized amount for a payment can be incremented if it is in status: requires_capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/incremental_authorization", + request_body=PaymentsIncrementalAuthorizationRequest, + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment authorized amount incremented"), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Increment authorized amount for a Payment", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsIncrementalAuthorization))] +pub async fn payments_incremental_authorization( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsIncrementalAuthorization; + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + payload.payment_id = payment_id; + let locking_action = payload.get_locking_input(flow.clone()); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req| { + payments::payments_core::< + api_types::IncrementalAuthorization, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentIncrementalAuthorization, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + HeaderPayload::default(), + ) + }, + &auth::ApiKeyAuth, + locking_action, + )) + .await +} + pub fn get_or_generate_payment_id( payload: &mut payment_types::PaymentsRequest, ) -> errors::RouterResult<()> { @@ -1135,3 +1196,19 @@ impl GetLockingInput for payment_types::PaymentsCaptureRequest { } } } + +impl GetLockingInput for payment_types::PaymentsIncrementalAuthorizationRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 1ff46474db59..ee5727bbda90 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -545,6 +545,9 @@ pub async fn send_request( Method::Put => client .put(url) .body(request.payload.expose_option().unwrap_or_default()), // If payload needs processing the body cannot have default + Method::Patch => client + .patch(url) + .body(request.payload.expose_option().unwrap_or_default()), Method::Delete => client.delete(url), } .add_headers(headers) @@ -1186,6 +1189,7 @@ impl Authenticate for api_models::payments::PaymentsSessionRequest { impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} +impl Authenticate for api_models::payments::PaymentsIncrementalAuthorizationRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} pub fn build_redirection_form( diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 595f487e7079..08cbb36952e3 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -52,6 +52,11 @@ pub type PaymentsBalanceRouterData = pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCaptureRouterData = RouterData; +pub type PaymentsIncrementalAuthorizationRouterData = RouterData< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type PaymentsCancelRouterData = RouterData; pub type PaymentsRejectRouterData = RouterData; @@ -142,6 +147,11 @@ pub type TokenizationType = dyn services::ConnectorIntegration< PaymentMethodTokenizationData, PaymentsResponseData, >; +pub type IncrementalAuthorizationType = dyn services::ConnectorIntegration< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type ConnectorCustomerType = dyn services::ConnectorIntegration< api::CreateConnectorCustomer, @@ -395,6 +405,15 @@ pub struct PaymentsCaptureData { pub browser_info: Option, } +#[derive(Debug, Clone, Default)] +pub struct PaymentsIncrementalAuthorizationData { + pub total_amount: i64, + pub additional_amount: i64, + pub currency: storage_enums::Currency, + pub reason: Option, + pub connector_transaction_id: String, +} + #[allow(dead_code)] #[derive(Debug, Clone, Default)] pub struct MultipleCaptureRequestData { @@ -599,6 +618,7 @@ impl Capturable for PaymentsCancelData { impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} +impl Capturable for PaymentsIncrementalAuthorizationData {} impl Capturable for PaymentsSyncData { fn get_capture_amount(&self, payment_data: &PaymentData) -> Option where @@ -707,6 +727,12 @@ pub enum PaymentsResponseData { session_token: Option, connector_response_reference_id: Option, }, + IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus, + connector_authorization_id: Option, + error_code: Option, + error_message: Option, + }, } #[derive(Debug, Clone)] diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index b00a7f0cbdac..2acf42fa479d 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -5,11 +5,12 @@ pub use api_models::payments::{ PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsRedirectRequest, - PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, - PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, - TimeRange, UrlDetails, VerifyRequest, VerifyResponse, WalletData, + PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, + PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, + VerifyRequest, VerifyResponse, WalletData, }; use error_stack::{IntoReport, ResultExt}; @@ -81,6 +82,9 @@ pub struct SetupMandate; #[derive(Debug, Clone)] pub struct PreProcessing; +#[derive(Debug, Clone)] +pub struct IncrementalAuthorization; + pub trait PaymentIdTypeExt { fn get_payment_intent_id(&self) -> errors::CustomResult; } @@ -164,6 +168,15 @@ pub trait MandateSetup: { } +pub trait PaymentIncrementalAuthorization: + api::ConnectorIntegration< + IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, +> +{ +} + pub trait PaymentsCompleteAuthorize: api::ConnectorIntegration< CompleteAuthorize, @@ -215,6 +228,7 @@ pub trait Payment: + PaymentToken + PaymentsPreProcessing + ConnectorCustomer + + PaymentIncrementalAuthorization { } diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index a83a405f3554..c8cc7f9c010f 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod authorization; pub mod business_profile; pub mod capture; pub mod cards_info; @@ -43,7 +44,7 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, diff --git a/crates/router/src/types/storage/authorization.rs b/crates/router/src/types/storage/authorization.rs new file mode 100644 index 000000000000..678cd64f8810 --- /dev/null +++ b/crates/router/src/types/storage/authorization.rs @@ -0,0 +1 @@ +pub use diesel_models::authorization::{Authorization, AuthorizationNew, AuthorizationUpdate}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 99096864a000..0244d8dc18ef 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -685,6 +685,19 @@ impl ForeignFrom for api_models::disputes::DisputeResponse { } } +impl ForeignFrom for payments::IncrementalAuthorizationResponse { + fn foreign_from(authorization: storage::Authorization) -> Self { + Self { + authorization_id: authorization.authorization_id, + amount: authorization.amount, + status: authorization.status, + error_code: authorization.error_code, + error_message: authorization.error_message, + previously_authorized_amount: authorization.previously_authorized_amount, + } + } +} + impl ForeignFrom for api_models::disputes::DisputeResponsePaymentsRetrieve { fn foreign_from(dispute: storage::Dispute) -> Self { Self { diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 7a9cf6d2b7db..543e3cd2aa5f 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -196,6 +196,7 @@ pub async fn generate_sample_data( surcharge_applicable: Default::default(), request_incremental_authorization: Default::default(), incremental_authorization_allowed: Default::default(), + authorization_count: Default::default(), }; let payment_attempt = PaymentAttemptBatchNew { attempt_id: attempt_id.clone(), diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 823b3eae497d..7e5cfeb43974 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -542,6 +542,7 @@ pub trait ConnectorActions: Connector { Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } @@ -1029,6 +1030,7 @@ pub fn get_connector_transaction_id( Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 370e03b984ba..e743a2d9cc52 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -27,6 +27,8 @@ pub enum Derives { Verify, Session, SessionData, + IncrementalAuthorization, + IncrementalAuthorizationData, } impl Derives { @@ -95,6 +97,12 @@ impl Conversion { } Derives::Session => syn::Ident::new("PaymentsSessionRequest", Span::call_site()), Derives::SessionData => syn::Ident::new("PaymentsSessionData", Span::call_site()), + Derives::IncrementalAuthorization => { + syn::Ident::new("PaymentsIncrementalAuthorizationRequest", Span::call_site()) + } + Derives::IncrementalAuthorizationData => { + syn::Ident::new("PaymentsIncrementalAuthorizationData", Span::call_site()) + } } } @@ -414,6 +422,7 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result syn::Result DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } @@ -1728,6 +1735,13 @@ impl DataModelExt for PaymentAttemptUpdate { connector, updated_by, }, + DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index fdf9875bc1ff..90bb21190c39 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -99,6 +99,7 @@ impl PaymentIntentInterface for KVRouterStore { surcharge_applicable: new.surcharge_applicable, request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, + authorization_count: new.authorization_count, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -762,6 +763,7 @@ impl DataModelExt for PaymentIntentNew { surcharge_applicable: self.surcharge_applicable, request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, + authorization_count: self.authorization_count, } } @@ -804,6 +806,7 @@ impl DataModelExt for PaymentIntentNew { surcharge_applicable: storage_model.surcharge_applicable, request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, } } } @@ -851,6 +854,7 @@ impl DataModelExt for PaymentIntent { surcharge_applicable: self.surcharge_applicable, request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, + authorization_count: self.authorization_count, } } @@ -894,6 +898,7 @@ impl DataModelExt for PaymentIntent { surcharge_applicable: storage_model.surcharge_applicable, request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, } } } @@ -1038,6 +1043,14 @@ impl DataModelExt for PaymentIntentUpdate { surcharge_applicable: Some(surcharge_applicable), updated_by, }, + Self::IncrementalAuthorizationAmountUpdate { amount } => { + DieselPaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } + } + Self::AuthorizationCountUpdate { + authorization_count, + } => DieselPaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + }, } } diff --git a/migrations/2023-11-30-170902_add-authorizations-table/down.sql b/migrations/2023-11-30-170902_add-authorizations-table/down.sql new file mode 100644 index 000000000000..476f16a52aab --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS incremental_authorization; \ No newline at end of file diff --git a/migrations/2023-11-30-170902_add-authorizations-table/up.sql b/migrations/2023-11-30-170902_add-authorizations-table/up.sql new file mode 100644 index 000000000000..ade615877dc2 --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS incremental_authorization ( + authorization_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + payment_id VARCHAR(64) NOT NULL, + amount BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + status VARCHAR(64) NOT NULL, + error_code VARCHAR(255), + error_message TEXT, + connector_authorization_id VARCHAR(64), + previously_authorized_amount BIGINT NOT NULL, + PRIMARY KEY (authorization_id, merchant_id) +); \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql new file mode 100644 index 000000000000..8d4d0e6d81d2 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS authorization_count; \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql new file mode 100644 index 000000000000..741135c6a1a3 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS authorization_count INTEGER; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index b1e313f15baa..4f26589ba029 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2888,6 +2888,15 @@ "no_three_ds" ] }, + "AuthorizationStatus": { + "type": "string", + "enum": [ + "success", + "failure", + "processing", + "unresolved" + ] + }, "BacsBankTransfer": { "type": "object", "required": [ @@ -6419,6 +6428,44 @@ } } }, + "IncrementalAuthorizationResponse": { + "type": "object", + "required": [ + "authorization_id", + "amount", + "status", + "previously_authorized_amount" + ], + "properties": { + "authorization_id": { + "type": "string", + "description": "The unique identifier of authorization" + }, + "amount": { + "type": "integer", + "format": "int64", + "description": "Amount the authorization has been made for" + }, + "status": { + "$ref": "#/components/schemas/AuthorizationStatus" + }, + "error_code": { + "type": "string", + "description": "Error code sent by the connector for authorization", + "nullable": true + }, + "error_message": { + "type": "string", + "description": "Error message sent by the connector for authorization", + "nullable": true + }, + "previously_authorized_amount": { + "type": "integer", + "format": "int64", + "description": "Previously authorized amount for the payment" + } + } + }, "IndomaretVoucherData": { "type": "object", "required": [ @@ -10540,6 +10587,20 @@ "type": "boolean", "description": "If true incremental authorization can be performed on this payment", "nullable": true + }, + "authorization_count": { + "type": "integer", + "format": "int32", + "description": "Total number of authorizations happened in an incremental_authorization payment", + "nullable": true + }, + "incremental_authorizations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncrementalAuthorizationResponse" + }, + "description": "List of incremental authorizations happened to the payment", + "nullable": true } } }, From 6c7d3a2e8a047ff23b52b76792fe8f28d3b952a4 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:51:41 +0530 Subject: [PATCH 139/443] fix: use card bin to get additional card details (#3036) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 2 + crates/api_models/src/payments.rs | 42 ++++++++++ .../src/surcharge_decision_configs.rs | 1 - crates/router/src/core/payments.rs | 50 ++---------- crates/router/src/core/payments/helpers.rs | 29 ++----- crates/router/src/core/payments/operations.rs | 1 - .../payments/operations/payment_confirm.rs | 20 ++++- .../payments/operations/payment_create.rs | 81 +++++++++++-------- crates/router/src/core/payments/types.rs | 2 + 9 files changed, 125 insertions(+), 103 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 3343becaaae6..b3c6b049d5d9 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -342,6 +342,8 @@ pub struct SurchargeDetailsResponse { pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment pub display_tax_on_surcharge_amount: f64, + /// sum of display_surcharge_amount and display_tax_on_surcharge_amount + pub display_total_surcharge_amount: f64, /// sum of original amount, pub display_final_amount: f64, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0da6822f1501..93c97cbd443c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -709,6 +709,33 @@ pub struct Card { pub nick_name: Option>, } +impl Card { + fn apply_additional_card_info(&self, additional_card_info: AdditionalCardInfo) -> Self { + Self { + card_number: self.card_number.clone(), + card_exp_month: self.card_exp_month.clone(), + card_exp_year: self.card_exp_year.clone(), + card_holder_name: self.card_holder_name.clone(), + card_cvc: self.card_cvc.clone(), + card_issuer: self + .card_issuer + .clone() + .or(additional_card_info.card_issuer), + card_network: self + .card_network + .clone() + .or(additional_card_info.card_network), + card_type: self.card_type.clone().or(additional_card_info.card_type), + card_issuing_country: self + .card_issuing_country + .clone() + .or(additional_card_info.card_issuing_country), + bank_code: self.bank_code.clone().or(additional_card_info.bank_code), + nick_name: self.nick_name.clone(), + } + } +} + #[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, Default)] #[serde(rename_all = "snake_case")] pub struct CardToken { @@ -882,6 +909,21 @@ impl PaymentMethodData { | Self::CardToken(_) => None, } } + pub fn apply_additional_payment_data( + &self, + additional_payment_data: AdditionalPaymentData, + ) -> Self { + if let AdditionalPaymentData::Card(additional_card_info) = additional_payment_data { + match self { + Self::Card(card) => { + Self::Card(card.apply_additional_card_info(*additional_card_info)) + } + _ => self.to_owned(), + } + } else { + self.to_owned() + } + } } pub trait GetPaymentMethodType { diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs index 7ead27945584..0777bde85de0 100644 --- a/crates/api_models/src/surcharge_decision_configs.rs +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -30,7 +30,6 @@ impl EuclidDirFilter for SurchargeDecisionConfigs { DirKeyKind::PaymentAmount, DirKeyKind::PaymentCurrency, DirKeyKind::BillingCountry, - DirKeyKind::CardType, DirKeyKind::CardNetwork, DirKeyKind::PayLaterType, DirKeyKind::WalletType, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8bc251f9c3d3..2a3ed0f1596f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -13,10 +13,7 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; -use api_models::{ - self, enums, - payments::{self, HeaderPayload}, -}; +use api_models::{self, enums, payments::HeaderPayload}; use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; @@ -176,10 +173,6 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { - operation - .to_domain()? - .populate_payment_data(state, &mut payment_data, &req, &merchant_account) - .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -406,7 +399,6 @@ where async fn populate_surcharge_details( state: &AppState, payment_data: &mut PaymentData, - request: &payments::PaymentsRequest, ) -> RouterResult<()> where F: Send + Clone, @@ -416,7 +408,7 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = request + let payment_method_data = payment_data .payment_method_data .clone() .get_required_value("payment_method_data")?; @@ -437,39 +429,7 @@ where Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, }; - let request_surcharge_details = request.surcharge_details; - - match (request_surcharge_details, calculated_surcharge_details) { - (Some(request_surcharge_details), Some(calculated_surcharge_details)) => { - if calculated_surcharge_details - .is_request_surcharge_matching(request_surcharge_details) - { - payment_data.surcharge_details = Some(calculated_surcharge_details); - } else { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), - } - .into()); - } - } - (None, Some(_calculated_surcharge_details)) => { - return Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "surcharge_details", - } - .into()); - } - (Some(request_surcharge_details), None) => { - if request_surcharge_details.is_surcharge_zero() { - return Ok(()); - } else { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), - } - .into()); - } - } - (None, None) => return Ok(()), - }; + payment_data.surcharge_details = calculated_surcharge_details; } else { let surcharge_details = payment_data @@ -978,6 +938,10 @@ where payment_data, ) .await?; + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; let mut router_data = payment_data .construct_router_data( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 3e01a7b193d0..c9ab77c6a332 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3634,31 +3634,16 @@ pub fn get_key_params_for_surcharge_details( )> { match payment_method_data { api_models::payments::PaymentMethodData::Card(card) => { - let card_type = card - .card_type - .get_required_value("payment_method_data.card.card_type")?; let card_network = card .card_network .get_required_value("payment_method_data.card.card_network")?; - match card_type.to_lowercase().as_str() { - "credit" => Ok(( - common_enums::PaymentMethod::Card, - common_enums::PaymentMethodType::Credit, - Some(card_network), - )), - "debit" => Ok(( - common_enums::PaymentMethod::Card, - common_enums::PaymentMethodType::Debit, - Some(card_network), - )), - _ => { - logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable"); - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data.card.card_type", - } - .into()) - } - } + // surcharge generated will always be same for credit as well as debit + // since surcharge conditions cannot be defined on card_type + Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + Some(card_network), + )) } api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( common_enums::PaymentMethod::CardRedirect, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 93db8f03ff5c..cf0c0ab294a8 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -159,7 +159,6 @@ pub trait Domain: Send + Sync { &'a self, _state: &AppState, _payment_data: &mut PaymentData, - _request: &R, _merchant_account: &domain::MerchantAccount, ) -> CustomResult<(), errors::ApiErrorResponse> { Ok(()) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 91dd5be40f64..578395860c9a 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -446,6 +446,21 @@ impl ) .await?; + let additional_pm_data = request + .payment_method_data + .as_ref() + .async_map(|payment_method_data| async { + helpers::get_additional_payment_data(payment_method_data, &*state.store).await + }) + .await; + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_pm_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -462,7 +477,7 @@ impl billing: billing_address.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: payment_method_data_after_card_bin_call, force_sync: None, refunds: vec![], disputes: vec![], @@ -593,10 +608,9 @@ impl Domain, - request: &api::PaymentsRequest, _merchant_account: &domain::MerchantAccount, ) -> CustomResult<(), errors::ApiErrorResponse> { - populate_surcharge_details(state, payment_data, request).await + populate_surcharge_details(state, payment_data).await } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index f3cd726a17c7..287e4945951b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -167,7 +167,7 @@ impl ) .await?; - let payment_attempt_new = Self::make_payment_attempt( + let (payment_attempt_new, additional_payment_data) = Self::make_payment_attempt( &payment_id, merchant_id, money, @@ -290,6 +290,14 @@ impl payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_payment_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -306,7 +314,7 @@ impl billing: billing_address.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: payment_method_data_after_card_bin_call, refunds: vec![], disputes: vec![], attempts: None, @@ -604,7 +612,10 @@ impl PaymentCreate { request: &api::PaymentsRequest, browser_info: Option, state: &AppState, - ) -> RouterResult { + ) -> RouterResult<( + storage::PaymentAttemptNew, + Option, + )> { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let status = helpers::payment_attempt_status_fsm(&request.payment_method_data, request.confirm); @@ -616,7 +627,8 @@ impl PaymentCreate { .async_map(|payment_method_data| async { helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) - .await + .await; + let additional_pm_data_value = additional_pm_data .as_ref() .map(Encode::::encode_to_value) .transpose() @@ -631,35 +643,38 @@ impl PaymentCreate { utils::get_payment_attempt_id(payment_id, 1) }; - Ok(storage::PaymentAttemptNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - attempt_id, - status, - currency, - amount: amount.into(), - payment_method, - capture_method: request.capture_method, - capture_on: request.capture_on, - confirm: request.confirm.unwrap_or(false), - created_at, - modified_at, - last_synced, - authentication_type: request.authentication_type, - browser_info, - payment_experience: request.payment_experience, - payment_method_type, - payment_method_data: additional_pm_data, - amount_to_capture: request.amount_to_capture, - payment_token: request.payment_token.clone(), - mandate_id: request.mandate_id.clone(), - business_sub_label: request.business_sub_label.clone(), - mandate_details: request - .mandate_data - .as_ref() - .and_then(|inner| inner.mandate_type.clone().map(Into::into)), - ..storage::PaymentAttemptNew::default() - }) + Ok(( + storage::PaymentAttemptNew { + payment_id: payment_id.to_string(), + merchant_id: merchant_id.to_string(), + attempt_id, + status, + currency, + amount: amount.into(), + payment_method, + capture_method: request.capture_method, + capture_on: request.capture_on, + confirm: request.confirm.unwrap_or(false), + created_at, + modified_at, + last_synced, + authentication_type: request.authentication_type, + browser_info, + payment_experience: request.payment_experience, + payment_method_type, + payment_method_data: additional_pm_data_value, + amount_to_capture: request.amount_to_capture, + payment_token: request.payment_token.clone(), + mandate_id: request.mandate_id.clone(), + business_sub_label: request.business_sub_label.clone(), + mandate_details: request + .mandate_data + .as_ref() + .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + ..storage::PaymentAttemptNew::default() + }, + additional_pm_data, + )) } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index 7a50254e6f05..f97cdc17d724 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -219,6 +219,8 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), display_surcharge_amount, display_tax_on_surcharge_amount, + display_total_surcharge_amount: display_surcharge_amount + + display_tax_on_surcharge_amount, display_final_amount, }) } From daf0f09f8e3293ee6a3599a25362d9171fc5b2e7 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:30:51 +0530 Subject: [PATCH 140/443] feat: calculate surcharge for customer saved card list (#3039) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/api_models/src/payment_methods.rs | 68 +++++---- crates/common_utils/Cargo.toml | 1 + .../router/src/core/payment_methods/cards.rs | 108 ++++++++++++-- .../surcharge_decision_configs.rs | 93 ++++++++++-- crates/router/src/core/payments.rs | 77 ++++++---- crates/router/src/core/payments/helpers.rs | 3 +- .../payments/operations/payment_confirm.rs | 77 +--------- .../payments/operations/payment_create.rs | 2 +- .../payments/operations/payment_update.rs | 2 +- crates/router/src/core/payments/types.rs | 139 ++++++++++++------ crates/router/src/core/utils.rs | 74 +--------- crates/router/src/openapi.rs | 5 +- crates/router/src/types/api.rs | 6 +- openapi/openapi_spec.json | 108 ++++++++++++++ 15 files changed, 477 insertions(+), 287 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8719b29f51d..4231c62d9499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,6 +1708,7 @@ dependencies = [ "thiserror", "time", "tokio 1.32.0", + "utoipa", ] [[package]] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b3c6b049d5d9..84830498b344 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -8,7 +8,7 @@ use common_utils::{ types::{Percentage, Surcharge}, }; use serde::de; -use utoipa::ToSchema; +use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; @@ -264,19 +264,6 @@ pub struct CardNetworkTypes { pub card_network: api_enums::CardNetwork, /// surcharge details for this card network - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// The list of eligible connectors for a given card network @@ -313,31 +300,19 @@ pub struct ResponsePaymentMethodTypes { pub required_fields: Option>, /// surcharge details for this payment method type if exists - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// auth service connector label for this payment method type, if exists pub pm_auth_connector: Option, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] + +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct SurchargeDetailsResponse { /// surcharge value - pub surcharge: Surcharge, + pub surcharge: SurchargeResponse, /// tax on surcharge value - pub tax_on_surcharge: Option>, + pub tax_on_surcharge: Option, /// surcharge amount for this payment pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment @@ -348,6 +323,36 @@ pub struct SurchargeDetailsResponse { pub display_final_amount: f64, } +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum SurchargeResponse { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(SurchargePercentage), +} + +impl From for SurchargeResponse { + fn from(value: Surcharge) -> Self { + match value { + Surcharge::Fixed(amount) => Self::Fixed(amount), + Surcharge::Rate(percentage) => Self::Rate(percentage.into()), + } + } +} + +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct SurchargePercentage { + percentage: f32, +} + +impl From> for SurchargePercentage { + fn from(value: Percentage) -> Self { + Self { + percentage: value.get_percentage(), + } + } +} /// Required fields info used while listing the payment_method_data #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, ToSchema, Hash)] pub struct RequiredFieldInfo { @@ -716,6 +721,9 @@ pub struct CustomerPaymentMethod { #[schema(example = json!({"mask": "0000"}))] pub bank: Option, + /// Surcharge details for this saved card + pub surcharge_details: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3619c93d772c..3a41b111b39d 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -38,6 +38,7 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } +utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 545733e298ab..bbcfe45a1d0c 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -25,7 +25,10 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; -use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; +use super::surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, +}; use crate::{ configs::settings, core::{ @@ -38,7 +41,6 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, - utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -1687,12 +1689,9 @@ pub async fn call_surcharge_decision_management( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; if !surcharge_results.is_empty_result() { - persist_individual_surcharge_details_in_redis( - &state, - merchant_account, - &surcharge_results, - ) - .await?; + surcharge_results + .persist_individual_surcharge_details_in_redis(&state, merchant_account) + .await?; let _ = state .store .update_payment_intent( @@ -1711,6 +1710,56 @@ pub async fn call_surcharge_decision_management( } } +pub async fn call_surcharge_decision_management_for_saved_card( + state: &routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + customer_payment_method_response: &mut api::CustomerPaymentMethodsListResponse, +) -> errors::RouterResult<()> { + if payment_attempt.surcharge_amount.is_some() { + Ok(()) + } else { + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = perform_surcharge_decision_management_for_saved_cards( + state, + algorithm_ref, + payment_attempt, + &payment_intent, + &mut customer_payment_method_response.customer_payment_methods, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); + } + Ok(()) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, @@ -2195,12 +2244,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); - let payment_intent = helpers::verify_payment_intent_time_and_client_secret( - db, - &merchant_account, - cloned_secret, - ) - .await?; + let payment_intent: Option = + helpers::verify_payment_intent_time_and_client_secret( + db, + &merchant_account, + cloned_secret, + ) + .await?; let customer_id = payment_intent .as_ref() .and_then(|intent| intent.customer_id.to_owned()) @@ -2326,6 +2376,7 @@ pub async fn list_customer_payment_method( created: Some(pm.created_at), bank_transfer: pmd, bank: bank_details, + surcharge_details: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2377,9 +2428,36 @@ pub async fn list_customer_payment_method( } } - let response = api::CustomerPaymentMethodsListResponse { + let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, }; + let payment_attempt = payment_intent + .as_ref() + .async_map(|payment_intent| async { + state + .store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_account.merchant_id, + &payment_intent.active_attempt.get_id(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + }) + .await + .transpose()?; + + if let Some((payment_attempt, payment_intent)) = payment_attempt.zip(payment_intent) { + call_surcharge_decision_management_for_saved_card( + state, + &merchant_account, + &payment_attempt, + payment_intent, + &mut response, + ) + .await?; + } Ok(services::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 38ae71754b87..e130795e945a 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -112,9 +112,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + Some(card_network_type.card_network.clone()), + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -138,9 +140,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + None, + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -201,15 +205,82 @@ where let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details_response = get_surcharge_details_from_surcharge_output( + let surcharge_details = get_surcharge_details_from_surcharge_output( surcharge_details, &payment_data.payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_method_type.to_owned().into(), - payment_method_type, - None, - surcharge_details_response, + types::SurchargeKey::PaymentMethodData( + payment_method_type.to_owned().into(), + *payment_method_type, + None, + ), + surcharge_details, + ); + } + } + Ok(surcharge_metadata) +} +pub async fn perform_surcharge_decision_management_for_saved_cards( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +) -> ConditionalConfigResult { + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(surcharge_metadata); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + + for customer_payment_method in customer_payment_method_list.iter_mut() { + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method_type = + customer_payment_method.payment_method_type; + backend_input.payment_method.card_network = customer_payment_method + .card + .as_ref() + .and_then(|card| card.scheme.as_ref()) + .map(|scheme| { + scheme + .clone() + .parse_enum("CardNetwork") + .change_context(ConfigError::DslExecutionError) + }) + .transpose()?; + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details_output) = surcharge_output.surcharge_details { + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details_output, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + surcharge_details.clone(), + ); + customer_payment_method.surcharge_details = Some( + SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) + .into_report() + .change_context(ConfigError::DslParsingError)?, ); } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2a3ed0f1596f..16fda276f6a5 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -39,11 +39,8 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, - types::SurchargeDetails, -}; -use super::{ - errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, }; +use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -408,26 +405,39 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = payment_data + let raw_card_key = payment_data .payment_method_data - .clone() - .get_required_value("payment_method_data")?; - let (payment_method, payment_method_type, card_network) = - get_key_params_for_surcharge_details(payment_method_data)?; + .as_ref() + .map(get_key_params_for_surcharge_details) + .transpose()? + .map(|(payment_method, payment_method_type, card_network)| { + types::SurchargeKey::PaymentMethodData( + payment_method, + payment_method_type, + card_network, + ) + }); + let saved_card_key = payment_data.token.clone().map(types::SurchargeKey::Token); - let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis( - state, - &payment_method, - &payment_method_type, - card_network, - &payment_data.payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => Some(surcharge_details), - Err(err) if err.current_context() == &RedisError::NotFound => None, - Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, - }; + let surcharge_key = raw_card_key + .or(saved_card_key) + .get_required_value("payment_method_data or payment_token")?; + logger::debug!(surcharge_key_confirm =? surcharge_key); + + let calculated_surcharge_details = + match types::SurchargeMetadata::get_individual_surcharge_detail_from_redis( + state, + surcharge_key, + &payment_data.payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => Some(surcharge_details), + Err(err) if err.current_context() == &RedisError::NotFound => None, + Err(err) => { + Err(err).change_context(errors::ApiErrorResponse::InternalServerError)? + } + }; payment_data.surcharge_details = calculated_surcharge_details; } else { @@ -436,7 +446,10 @@ where .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) + types::SurchargeDetails::from(( + &surcharge_details, + &payment_data.payment_attempt, + )) }); payment_data.surcharge_details = surcharge_details; } @@ -469,7 +482,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetails { + types::SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -501,12 +514,9 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; - core_utils::persist_individual_surcharge_details_in_redis( - state, - merchant_account, - &surcharge_results, - ) - .await?; + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; Ok(if surcharge_results.is_empty_result() { None @@ -917,6 +927,11 @@ where merchant_connector_account.get_mca_id(); } + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, @@ -1846,7 +1861,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, pub incremental_authorization_details: Option, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c9ab77c6a332..4e491964e96c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3626,7 +3626,7 @@ impl ApplePayData { } pub fn get_key_params_for_surcharge_details( - payment_method_data: api_models::payments::PaymentMethodData, + payment_method_data: &api_models::payments::PaymentMethodData, ) -> RouterResult<( common_enums::PaymentMethod, common_enums::PaymentMethodType, @@ -3636,6 +3636,7 @@ pub fn get_key_params_for_surcharge_details( api_models::payments::PaymentMethodData::Card(card) => { let card_network = card .card_network + .clone() .get_required_value("payment_method_data.card.card_network")?; // surcharge generated will always be same for credit as well as debit // since surcharge conditions cannot be defined on card_type diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 578395860c9a..af2a9fa49c8b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -5,7 +5,6 @@ use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; -use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use tracing_futures::Instrument; @@ -19,7 +18,7 @@ use crate::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils, get_individual_surcharge_detail_from_redis}, + utils::{self as core_utils}, }, db::StorageInterface, routes::AppState, @@ -439,13 +438,6 @@ impl sm }); - Self::validate_request_surcharge_details_with_session_surcharge_details( - state, - &payment_attempt, - request, - ) - .await?; - let additional_pm_data = request .payment_method_data .as_ref() @@ -904,70 +896,3 @@ impl ValidateRequest RouterResult<()> { - match ( - request.surcharge_details, - request.payment_method_data.as_ref(), - ) { - (Some(request_surcharge_details), Some(payment_method_data)) => { - if let Some(payment_method_type) = - payment_method_data.get_payment_method_type_if_session_token_type() - { - let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { - message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), - }.into()); - if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { - // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create - // if surcharge was sent in payment create call, the same would have been sent to the connector during session call - // So verify the same - if request_surcharge_details.surcharge_amount != attempt_surcharge_amount - || request_surcharge_details.tax_amount != payment_attempt.tax_amount - { - return invalid_surcharge_details_error; - } - } else { - // if not sent in payment create - // verify that any calculated surcharge sent in session flow is same as the one sent in confirm - return match get_individual_surcharge_detail_from_redis( - state, - &payment_method_type.into(), - &payment_method_type, - None, - &payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => utils::when( - !surcharge_details - .is_request_surcharge_matching(request_surcharge_details), - || invalid_surcharge_details_error, - ), - Err(err) if err.current_context() == &RedisError::NotFound => { - utils::when(!request_surcharge_details.is_surcharge_zero(), || { - invalid_surcharge_details_error - }) - } - Err(err) => Err(err) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch redis value"), - }; - } - } - Ok(()) - } - (Some(_request_surcharge_details), None) => { - Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data", - } - .into()) - } - _ => Ok(()), - } - } -} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 287e4945951b..eb7f31ba24d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -287,7 +287,7 @@ impl let setup_mandate = setup_mandate.map(MandateData::from); let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_method_data_after_card_bin_call = request diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e1c373171682..f1a35cffce87 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -337,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f97cdc17d724..001082d2c92e 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -1,13 +1,18 @@ use std::{collections::HashMap, num::TryFromIntError}; use api_models::{payment_methods::SurchargeDetailsResponse, payments::RequestSurchargeDetails}; -use common_utils::{consts, types as common_types}; +use common_utils::{consts, errors::CustomResult, ext_traits::Encode, types as common_types}; use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::{IntoReport, ResultExt}; +use redis_interface::errors::RedisError; +use router_env::{instrument, tracing}; use crate::{ + consts as router_consts, core::errors::{self, RouterResult}, + routes::AppState, types::{ + domain, storage::{self, enums as storage_enums}, transformers::ForeignTryFrom, }, @@ -215,8 +220,8 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe let display_final_amount = currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; Ok(Self { - surcharge: surcharge_details.surcharge.clone(), - tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + surcharge: surcharge_details.surcharge.clone().into(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into), display_surcharge_amount, display_tax_on_surcharge_amount, display_total_surcharge_amount: display_surcharge_amount @@ -239,16 +244,19 @@ impl SurchargeDetails { } } +#[derive(Eq, Hash, PartialEq, Clone, Debug, strum::Display)] +pub enum SurchargeKey { + Token(String), + PaymentMethodData( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), +} + #[derive(Clone, Debug)] pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetails, - >, + surcharge_results: HashMap, pub payment_attempt_id: String, } @@ -267,30 +275,14 @@ impl SurchargeMetadata { } pub fn insert_surcharge_details( &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, + surcharge_key: SurchargeKey, surcharge_details: SurchargeDetails, ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); + self.surcharge_results + .insert(surcharge_key, surcharge_details); } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetails> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) + pub fn get_surcharge_details(&self, surcharge_key: SurchargeKey) -> Option<&SurchargeDetails> { + self.surcharge_results.get(&surcharge_key) } pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { format!("surcharge_metadata_{}", payment_attempt_id) @@ -298,25 +290,78 @@ impl SurchargeMetadata { pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { self.surcharge_results .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + .map(|(surcharge_key, surcharge_details)| { + let key = Self::get_surcharge_details_redis_hashset_key(surcharge_key); (key, surcharge_details.to_owned()) }) .collect() } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) + pub fn get_surcharge_details_redis_hashset_key(surcharge_key: &SurchargeKey) -> String { + match surcharge_key { + SurchargeKey::Token(token) => { + format!("token_{}", token) + } + SurchargeKey::PaymentMethodData(payment_method, payment_method_type, card_network) => { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } } } + #[instrument(skip_all)] + pub async fn persist_individual_surcharge_details_in_redis( + &self, + state: &AppState, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult<()> { + if !self.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(&self.payment_attempt_id); + + let mut value_list = Vec::with_capacity(self.get_surcharge_results_size()); + for (key, value) in self.get_individual_surcharge_key_value_pairs().into_iter() { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = merchant_account + .intent_fulfillment_time + .unwrap_or(router_consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) + } + + #[instrument(skip_all)] + pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + surcharge_key: SurchargeKey, + payment_attempt_id: &str, + ) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = Self::get_surcharge_details_redis_hashset_key(&surcharge_key); + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") + .await + } } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 6d82c44d803a..724a698ff700 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,17 +4,12 @@ use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{ - errors::CustomResult, - ext_traits::{AsyncExt, Encode}, -}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{report, IntoReport, ResultExt}; -use euclid::enums as euclid_enums; -use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, types as payments_types, PaymentAddress}; +use super::payments::{helpers, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1068,71 +1063,6 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } -#[instrument(skip_all)] -pub async fn persist_individual_surcharge_details_in_redis( - state: &AppState, - merchant_account: &domain::MerchantAccount, - surcharge_metadata: &payments_types::SurchargeMetadata, -) -> RouterResult<()> { - if !surcharge_metadata.is_empty_result() { - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( - &surcharge_metadata.payment_attempt_id, - ); - - let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); - for (key, value) in surcharge_metadata - .get_individual_surcharge_key_value_pairs() - .into_iter() - { - value_list.push(( - key, - Encode::::encode_to_string_of_json(&value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode to string of json")?, - )); - } - let intent_fulfillment_time = merchant_account - .intent_fulfillment_time - .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); - redis_conn - .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to write to redis")?; - } - Ok(()) -} - -#[instrument(skip_all)] -pub async fn get_individual_surcharge_detail_from_redis( - state: &AppState, - payment_method: &euclid_enums::PaymentMethod, - payment_method_type: &euclid_enums::PaymentMethodType, - card_network: Option, - payment_attempt_id: &str, -) -> CustomResult { - let redis_conn = state - .store - .get_redis_conn() - .attach_printable("Failed to get redis connection")?; - let redis_key = - payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( - payment_method, - payment_method_type, - card_network.as_ref(), - ); - - redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") - .await -} - pub fn get_request_incremental_authorization_value( request_incremental_authorization: Option, capture_method: Option, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 82c98304c62b..d83117c59d76 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -319,6 +319,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, + api_models::payment_methods::SurchargeDetailsResponse, + api_models::payment_methods::SurchargeResponse, + api_models::payment_methods::SurchargePercentage, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, @@ -363,7 +366,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkObject + api_models::payments::PaymentLinkObject, )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index ea2ea8b701da..c74608ea20a1 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -238,7 +238,11 @@ impl SessionSurchargeDetails { ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata - .get_surcharge_details(payment_method, payment_method_type, card_network) + .get_surcharge_details(payments_types::SurchargeKey::PaymentMethodData( + *payment_method, + *payment_method_type, + card_network.cloned(), + )) .cloned(), Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4f26589ba029..d67089aea35f 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5116,6 +5116,14 @@ ], "nullable": true }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -12043,6 +12051,106 @@ } } }, + "SurchargeDetailsResponse": { + "type": "object", + "required": [ + "surcharge", + "display_surcharge_amount", + "display_tax_on_surcharge_amount", + "display_total_surcharge_amount", + "display_final_amount" + ], + "properties": { + "surcharge": { + "$ref": "#/components/schemas/SurchargeResponse" + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargePercentage" + } + ], + "nullable": true + }, + "display_surcharge_amount": { + "type": "number", + "format": "double", + "description": "surcharge amount for this payment" + }, + "display_tax_on_surcharge_amount": { + "type": "number", + "format": "double", + "description": "tax on surcharge amount for this payment" + }, + "display_total_surcharge_amount": { + "type": "number", + "format": "double", + "description": "sum of display_surcharge_amount and display_tax_on_surcharge_amount" + }, + "display_final_amount": { + "type": "number", + "format": "double", + "description": "sum of original amount," + } + } + }, + "SurchargePercentage": { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "number", + "format": "float" + } + } + }, + "SurchargeResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Fixed Surcharge value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "rate" + ] + }, + "value": { + "$ref": "#/components/schemas/SurchargePercentage" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "SwishQrData": { "type": "object" }, From 298e3627c379de5acfcafb074036754661801f1e Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Tue, 5 Dec 2023 01:47:55 +0530 Subject: [PATCH 141/443] fix: transform connector name to lowercase in connector integration script (#3048) --- scripts/add_connector.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 7ed5e65151e1..1246c51d8eb3 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -25,7 +25,7 @@ function find_prev_connector() { eval "$2='aci'" } -payment_gateway=$1; +payment_gateway=$(echo $1 | tr '[:upper:]' '[:lower:]') base_url=$2; payment_gateway_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${payment_gateway:0:1})${payment_gateway:1}" src="crates/router/src" @@ -49,7 +49,7 @@ git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/developm # Add enum for this connector in required places previous_connector='' -find_prev_connector $1 previous_connector +find_prev_connector $payment_gateway previous_connector previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connector:0:1})${previous_connector:1}" sed -i'' -e "s|pub mod $previous_connector;|pub mod $previous_connector;\npub mod ${payment_gateway};|" $conn.rs sed -i'' -e "s/};/${payment_gateway}::${payment_gateway_camelcase},\n};/" $conn.rs From ba392f58b2956d67e93a08853bcf2270a869be27 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 5 Dec 2023 12:47:37 +0530 Subject: [PATCH 142/443] fix: add fallback to reverselookup error (#3025) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/common_utils/src/lib.rs | 1 + crates/common_utils/src/macros.rs | 92 +++++++++++ crates/data_models/Cargo.toml | 1 + crates/data_models/src/errors.rs | 4 +- crates/diesel_models/src/errors.rs | 16 +- crates/router/src/db/refund.rs | 147 +++++++++--------- crates/router/src/db/user/sample_data.rs | 8 +- crates/router/src/lib.rs | 1 + crates/router/src/macros.rs | 72 +-------- crates/storage_impl/src/errors.rs | 10 +- crates/storage_impl/src/lib.rs | 10 +- .../src/payments/payment_attempt.rs | 125 +++++++++------ .../src/payments/payment_intent.rs | 27 ++-- 14 files changed, 288 insertions(+), 227 deletions(-) create mode 100644 crates/common_utils/src/macros.rs diff --git a/Cargo.lock b/Cargo.lock index 4231c62d9499..cb38c0b70b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,7 @@ dependencies = [ "async-trait", "common_enums", "common_utils", + "diesel_models", "error-stack", "masking", "serde", diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 62428dccfb6a..0ac8e886bc06 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -10,6 +10,7 @@ pub mod errors; pub mod events; pub mod ext_traits; pub mod fp_utils; +pub mod macros; pub mod pii; #[allow(missing_docs)] // Todo: add docs pub mod request; diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs new file mode 100644 index 000000000000..9d41569384f1 --- /dev/null +++ b/crates/common_utils/src/macros.rs @@ -0,0 +1,92 @@ +#![allow(missing_docs)] + +#[macro_export] +macro_rules! newtype_impl { + ($is_pub:vis, $name:ident, $ty_path:path) => { + impl std::ops::Deref for $name { + type Target = $ty_path; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl std::ops::DerefMut for $name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + impl From<$ty_path> for $name { + fn from(ty: $ty_path) -> Self { + Self(ty) + } + } + + impl $name { + pub fn into_inner(self) -> $ty_path { + self.0 + } + } + }; +} + +#[macro_export] +macro_rules! newtype { + ($is_pub:vis $name:ident = $ty_path:path) => { + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; + + ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { + #[derive($($trt),*)] + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; +} + +#[macro_export] +macro_rules! async_spawn { + ($t:block) => { + tokio::spawn(async move { $t }); + }; +} + +#[macro_export] +macro_rules! fallback_reverse_lookup_not_found { + ($a:expr,$b:expr) => { + match $a { + Ok(res) => res, + Err(err) => { + router_env::logger::error!(reverse_lookup_fallback = %err); + match err.current_context() { + errors::StorageError::ValueNotFound(_) => return $b, + errors::StorageError::DatabaseError(data_err) => { + match data_err.current_context() { + diesel_models::errors::DatabaseError::NotFound => return $b, + _ => return Err(err) + } + } + _=> return Err(err) + } + } + }; + }; +} + +#[macro_export] +macro_rules! collect_missing_value_keys { + [$(($key:literal, $option:expr)),+] => { + { + let mut keys: Vec<&'static str> = Vec::new(); + $( + if $option.is_none() { + keys.push($key); + } + )* + keys + } + }; +} diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 857d53b6999e..a86dc3070b4d 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -17,6 +17,7 @@ api_models = { version = "0.1.0", path = "../api_models" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } # Third party deps async-trait = "0.1.68" diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 4f8229ea0c9b..9616a3a944ca 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -1,3 +1,5 @@ +use diesel_models::errors::DatabaseError; + pub type StorageResult = error_stack::Result; #[derive(Debug, thiserror::Error)] @@ -6,7 +8,7 @@ pub enum StorageError { InitializationError, // TODO: deprecate this error type to use a domain error instead #[error("DatabaseError: {0:?}")] - DatabaseError(String), + DatabaseError(error_stack::Report), #[error("ValueNotFound: {0}")] ValueNotFound(String), #[error("DuplicateValue: {entity} already exists {key:?}")] diff --git a/crates/diesel_models/src/errors.rs b/crates/diesel_models/src/errors.rs index 0a8422131ae2..4a536aad07e4 100644 --- a/crates/diesel_models/src/errors.rs +++ b/crates/diesel_models/src/errors.rs @@ -1,4 +1,4 @@ -#[derive(Debug, thiserror::Error)] +#[derive(Copy, Clone, Debug, thiserror::Error)] pub enum DatabaseError { #[error("An error occurred when obtaining database connection")] DatabaseConnectionError, @@ -14,3 +14,17 @@ pub enum DatabaseError { #[error("An unknown error occurred")] Others, } + +impl From for DatabaseError { + fn from(error: diesel::result::Error) -> Self { + match error { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UniqueViolation, + _, + ) => Self::UniqueViolation, + diesel::result::Error::NotFound => Self::NotFound, + diesel::result::Error::QueryBuilderError(_) => Self::QueryGenerationFailed, + _ => Self::Others, + } + } +} diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 8ac8bd106eff..f385e1bc5a83 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -267,7 +267,7 @@ mod storage { #[cfg(feature = "kv_store")] mod storage { - use common_utils::date_time; + use common_utils::{date_time, fallback_reverse_lookup_not_found}; use error_stack::{IntoReport, ResultExt}; use redis_interface::HsetnxReply; use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; @@ -277,7 +277,6 @@ mod storage { connection, core::errors::{self, CustomResult}, db::reverse_lookup::ReverseLookupInterface, - logger, services::Store, types::storage::{self as storage_types, enums, kv}, utils::{self, db_utils}, @@ -304,10 +303,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{internal_reference_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_inter_ref_{merchant_id}_{internal_reference_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -382,6 +383,50 @@ mod storage { }, }; + let mut reverse_lookups = vec![ + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_ref_id_{}_{}", + created_refund.merchant_id, created_refund.refund_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + // [#492]: A discussion is required on whether this is required? + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_inter_ref_{}_{}", + created_refund.merchant_id, created_refund.internal_reference_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + ]; + if let Some(connector_refund_id) = created_refund.to_owned().connector_refund_id + { + reverse_lookups.push(storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_connector_{}_{}_{}", + created_refund.merchant_id, + connector_refund_id, + created_refund.connector + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }) + }; + let rev_look = reverse_lookups + .into_iter() + .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); + + futures::future::try_join_all(rev_look).await?; + match kv_wrapper::( self, KvOperation::::HSetNx( @@ -400,55 +445,7 @@ mod storage { key: Some(created_refund.refund_id), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - let mut reverse_lookups = vec![ - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, created_refund.refund_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - // [#492]: A discussion is required on whether this is required? - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, - created_refund.internal_reference_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - ]; - if let Some(connector_refund_id) = - created_refund.to_owned().connector_refund_id - { - reverse_lookups.push(storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}_{}", - created_refund.merchant_id, - connector_refund_id, - created_refund.connector - ), - pk_id: key, - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }) - }; - let rev_look = reverse_lookups - .into_iter() - .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); - - futures::future::try_join_all(rev_look).await?; - - Ok(created_refund) - } + Ok(HsetnxReply::KeySet) => Ok(created_refund), Err(er) => Err(er).change_context(errors::StorageError::KVError), } } @@ -475,17 +472,14 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); - let lookup = match self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await - { - Ok(l) => l, - Err(err) => { - logger::error!(?err); - return Ok(vec![]); - } - }; + let lookup_id = + format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); + let key = &lookup.pk_id; let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); @@ -575,10 +569,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{refund_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_ref_id_{merchant_id}_{refund_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -620,10 +616,13 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_refund_id}_{connector}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = + format!("ref_connector_{merchant_id}_{connector_refund_id}_{connector}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( diff --git a/crates/router/src/db/user/sample_data.rs b/crates/router/src/db/user/sample_data.rs index 11def9026854..ae98332cfc49 100644 --- a/crates/router/src/db/user/sample_data.rs +++ b/crates/router/src/db/user/sample_data.rs @@ -193,13 +193,7 @@ fn diesel_error_to_data_error(diesel_error: Report) -> Report { - StorageError::DatabaseError("No fields to update".to_string()) - } - DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - DatabaseError::Others => StorageError::DatabaseError("Others".to_string()), + err => StorageError::DatabaseError(error_stack::report!(*err)), }; diesel_error.change_context(new_err) } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 035314f71dfb..fb8be9636748 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -35,6 +35,7 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; +pub(crate) use self::macros::*; use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index 33ed43fcc7ab..e6c9dba7d6e2 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,68 +1,4 @@ -#[macro_export] -macro_rules! newtype_impl { - ($is_pub:vis, $name:ident, $ty_path:path) => { - impl std::ops::Deref for $name { - type Target = $ty_path; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl std::ops::DerefMut for $name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - impl From<$ty_path> for $name { - fn from(ty: $ty_path) -> Self { - Self(ty) - } - } - - impl $name { - pub fn into_inner(self) -> $ty_path { - self.0 - } - } - }; -} - -#[macro_export] -macro_rules! newtype { - ($is_pub:vis $name:ident = $ty_path:path) => { - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; - - ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { - #[derive($($trt),*)] - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; -} - -#[macro_export] -macro_rules! async_spawn { - ($t:block) => { - tokio::spawn(async move { $t }); - }; -} - -#[macro_export] -macro_rules! collect_missing_value_keys { - [$(($key:literal, $option:expr)),+] => { - { - let mut keys: Vec<&'static str> = Vec::new(); - $( - if $option.is_none() { - keys.push($key); - } - )* - keys - } - }; -} +pub use common_utils::{ + async_spawn, collect_missing_value_keys, fallback_reverse_lookup_not_found, newtype, + newtype_impl, +}; diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index bc68986cb8ea..105a93d4beae 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -92,15 +92,7 @@ impl Into for &StorageError { key: None, } } - storage_errors::DatabaseError::NoFieldsToUpdate => { - DataStorageError::DatabaseError("No fields to update".to_string()) - } - storage_errors::DatabaseError::QueryGenerationFailed => { - DataStorageError::DatabaseError("Query generation failed".to_string()) - } - storage_errors::DatabaseError::Others => { - DataStorageError::DatabaseError("Unknown database error".to_string()) - } + err => DataStorageError::DatabaseError(error_stack::report!(*err)), }, StorageError::ValueNotFound(i) => DataStorageError::ValueNotFound(i.clone()), StorageError::DuplicateValue { entity, key } => DataStorageError::DuplicateValue { diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index dc0dea4bb59c..7e2c7f2fc3c5 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -251,14 +251,6 @@ pub(crate) fn diesel_error_to_data_error( entity: "entity ", key: None, }, - diesel_models::errors::DatabaseError::NoFieldsToUpdate => { - StorageError::DatabaseError("No fields to update".to_string()) - } - diesel_models::errors::DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - diesel_models::errors::DatabaseError::Others => { - StorageError::DatabaseError("Others".to_string()) - } + _ => StorageError::DatabaseError(error_stack::report!(*diesel_error)), } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 9f351979f289..b524ff1aaa71 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1,5 +1,5 @@ use api_models::enums::{AuthenticationType, Connector, PaymentMethod, PaymentMethodType}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, fallback_reverse_lookup_not_found}; use data_models::{ errors, mandates::{MandateAmountData, MandateDataType}, @@ -399,6 +399,20 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; + //Reverse lookup for attempt_id + let reverse_lookup = ReverseLookupNew { + lookup_id: format!( + "pa_{}_{}", + &created_attempt.merchant_id, &created_attempt.attempt_id, + ), + pk_id: key.clone(), + sk_id: field.clone(), + source: "payment_attempt".to_string(), + updated_by: storage_scheme.to_string(), + }; + self.insert_reverse_lookup(reverse_lookup, storage_scheme) + .await?; + match kv_wrapper::( self, KvOperation::HSetNx( @@ -417,23 +431,7 @@ impl PaymentAttemptInterface for KVRouterStore { key: Some(key), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - //Reverse lookup for attempt_id - let reverse_lookup = ReverseLookupNew { - lookup_id: format!( - "{}_{}", - &created_attempt.merchant_id, &created_attempt.attempt_id, - ), - pk_id: key, - sk_id: field, - source: "payment_attempt".to_string(), - updated_by: storage_scheme.to_string(), - }; - self.insert_reverse_lookup(reverse_lookup, storage_scheme) - .await?; - - Ok(created_attempt) - } + Ok(HsetnxReply::KeySet) => Ok(created_attempt), Err(error) => Err(error.change_context(errors::StorageError::KVError)), } } @@ -480,16 +478,6 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; - kv_wrapper::<(), _, _>( - self, - KvOperation::Hset::((&field, redis_value), redis_entry), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - match ( old_connector_transaction_id, &updated_attempt.connector_transaction_id, @@ -549,6 +537,16 @@ impl PaymentAttemptInterface for KVRouterStore { (_, _) => {} } + kv_wrapper::<(), _, _>( + self, + KvOperation::Hset::((&field, redis_value), redis_entry), + &key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + Ok(updated_attempt) } } @@ -574,10 +572,20 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now - let lookup_id = format!("conn_trans_{merchant_id}_{connector_transaction_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -707,10 +715,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_txn_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_txn_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -799,10 +815,19 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{attempt_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_{merchant_id}_{attempt_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_attempt_id_merchant_id( + attempt_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( async { @@ -846,10 +871,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("preprocessing_{merchant_id}_{preprocessing_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_preprocessing_{merchant_id}_{preprocessing_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -1757,7 +1790,7 @@ async fn add_connector_txn_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("conn_trans_{}_{}", merchant_id, connector_transaction_id), + lookup_id: format!("pa_conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1779,7 +1812,7 @@ async fn add_preprocessing_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("preprocessing_{}_{}", merchant_id, preprocessing_id), + lookup_id: format!("pa_preprocessing_{}_{}", merchant_id, preprocessing_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 90bb21190c39..3e695947b8bf 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -494,12 +494,13 @@ impl PaymentIntentInterface for crate::RouterStore { .map(PaymentIntent::from_storage_model) .collect::>() }) - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } #[cfg(feature = "olap")] @@ -646,12 +647,13 @@ impl PaymentIntentInterface for crate::RouterStore { }) .collect() }) - .into_report() .map_err(|er| { - let new_er = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_er) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable("Error filtering payment records") + .into_report() } #[cfg(feature = "olap")] @@ -712,12 +714,13 @@ impl PaymentIntentInterface for crate::RouterStore { db_metrics::DatabaseOperation::Filter, ) .await - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } } From 6e09bc9e2c4bbe14dcb70da4a438850b03b3254c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:48:22 +0000 Subject: [PATCH 143/443] test(postman): update postman collection files --- postman/collection-json/adyen_uk.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 400f04241c27..04a7e39f15e7 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -13959,10 +13959,10 @@ "// Response body should have value \"invalid_request\" for \"error type\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'The payment has not succeeded yet. Please pass a successful payment to initiate refund'\",", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"The payment has not succeeded yet. Please pass a successful payment to initiate refund\",", + " \"This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured\",", " );", " },", " );", From 5b62731399c8d5b8bfd923fb61706b3a9c4f5ffe Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:48:22 +0000 Subject: [PATCH 144/443] chore(version): v1.95.0 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bbdac921fd7..3cd968293c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.95.0 (2023-12-05) + +### Features + +- **connector:** [BOA/CYBERSOURCE] Fix Status Mapping for Terminal St… ([#3031](https://github.com/juspay/hyperswitch/pull/3031)) ([`95876b0`](https://github.com/juspay/hyperswitch/commit/95876b0ce03e024edf77909502c53eb4e63a9855)) +- **pm_list:** Add required field for open_banking_uk for Adyen and Volt Connector ([#3032](https://github.com/juspay/hyperswitch/pull/3032)) ([`9d93533`](https://github.com/juspay/hyperswitch/commit/9d935332193dcc9f191a0a5a9e7405316794a418)) +- **router:** + - Add key_value to locker metrics ([#2995](https://github.com/juspay/hyperswitch/pull/2995)) ([`83fcd1a`](https://github.com/juspay/hyperswitch/commit/83fcd1a9deb106a44c8262923c7f1660b0c46bf2)) + - Add payments incremental authorization api ([#3038](https://github.com/juspay/hyperswitch/pull/3038)) ([`a0cfdd3`](https://github.com/juspay/hyperswitch/commit/a0cfdd3fb12f04b603f65551eac985c31e08da85)) +- **types:** Add email types for sending emails ([#3020](https://github.com/juspay/hyperswitch/pull/3020)) ([`c4bd47e`](https://github.com/juspay/hyperswitch/commit/c4bd47eca93a158c9daeeeb18afb1e735eea8c94)) +- **user:** + - Generate and delete sample data ([#2987](https://github.com/juspay/hyperswitch/pull/2987)) ([`092ec73`](https://github.com/juspay/hyperswitch/commit/092ec73b3c65ce6048d379383b078d643f0f35fc)) + - Add user_list and switch_list apis ([#3033](https://github.com/juspay/hyperswitch/pull/3033)) ([`ec15ddd`](https://github.com/juspay/hyperswitch/commit/ec15ddd0d0ed942fedec525406df3005d494b8d4)) +- Calculate surcharge for customer saved card list ([#3039](https://github.com/juspay/hyperswitch/pull/3039)) ([`daf0f09`](https://github.com/juspay/hyperswitch/commit/daf0f09f8e3293ee6a3599a25362d9171fc5b2e7)) + +### Bug Fixes + +- **connector:** [Paypal] Parse response for Cards with no 3DS check ([#3021](https://github.com/juspay/hyperswitch/pull/3021)) ([`d883cd1`](https://github.com/juspay/hyperswitch/commit/d883cd18972c5f9e8350e9a3f4e5cd56ec2c0787)) +- **pm_list:** [Trustpay]Update dynamic fields for trustpay blik ([#3042](https://github.com/juspay/hyperswitch/pull/3042)) ([`9274cef`](https://github.com/juspay/hyperswitch/commit/9274cefbdd29d2ac64baeea2fe504dff2472cb47)) +- **wasm:** Fix wasm function to return the categories for keys with their description respectively ([#3023](https://github.com/juspay/hyperswitch/pull/3023)) ([`2ac5b2c`](https://github.com/juspay/hyperswitch/commit/2ac5b2cd764c0aad53ac7c672dfcc9132fa5668f)) +- Use card bin to get additional card details ([#3036](https://github.com/juspay/hyperswitch/pull/3036)) ([`6c7d3a2`](https://github.com/juspay/hyperswitch/commit/6c7d3a2e8a047ff23b52b76792fe8f28d3b952a4)) +- Transform connector name to lowercase in connector integration script ([#3048](https://github.com/juspay/hyperswitch/pull/3048)) ([`298e362`](https://github.com/juspay/hyperswitch/commit/298e3627c379de5acfcafb074036754661801f1e)) +- Add fallback to reverselookup error ([#3025](https://github.com/juspay/hyperswitch/pull/3025)) ([`ba392f5`](https://github.com/juspay/hyperswitch/commit/ba392f58b2956d67e93a08853bcf2270a869be27)) + +### Refactors + +- **payment_methods:** Add support for passing card_cvc in payment_method_data object along with token ([#3024](https://github.com/juspay/hyperswitch/pull/3024)) ([`3ce04ab`](https://github.com/juspay/hyperswitch/commit/3ce04abae4eddfa27025368f5ef28987cccea43d)) +- **users:** Separate signup and signin ([#2921](https://github.com/juspay/hyperswitch/pull/2921)) ([`80efeb7`](https://github.com/juspay/hyperswitch/commit/80efeb76b1801529766978af1c06e2d2c7de66c0)) +- Create separate struct for surcharge details response ([#3027](https://github.com/juspay/hyperswitch/pull/3027)) ([`57591f8`](https://github.com/juspay/hyperswitch/commit/57591f819c7994099e76cff1affc7bcf3e45a031)) + +### Testing + +- **postman:** Update postman collection files ([`6e09bc9`](https://github.com/juspay/hyperswitch/commit/6e09bc9e2c4bbe14dcb70da4a438850b03b3254c)) + +**Full Changelog:** [`v1.94.0...v1.95.0`](https://github.com/juspay/hyperswitch/compare/v1.94.0...v1.95.0) + +- - - + + ## 1.94.0 (2023-12-01) ### Features From 1c3d260dc3e18fbf6cbd5122122a6c73dceb39a3 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:10:17 +0530 Subject: [PATCH 145/443] feat(user): add email apis and new enums for metadata (#3053) Co-authored-by: Rachit Naithani Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Rachit Naithani <81706961+racnan@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 9 +- crates/api_models/src/refunds.rs | 2 +- crates/api_models/src/user.rs | 23 +++ .../api_models/src/user/dashboard_metadata.rs | 41 +++- crates/diesel_models/src/enums.rs | 3 + .../src/query/dashboard_metadata.rs | 45 +++-- crates/router/src/core/errors/user.rs | 13 ++ crates/router/src/core/user.rs | 181 +++++++++++++++++- .../src/core/user/dashboard_metadata.rs | 136 ++++++++++++- crates/router/src/db/refund.rs | 4 +- crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/user.rs | 57 ++++++ crates/router/src/services/email/types.rs | 4 + crates/router/src/types/domain/user.rs | 42 ++++ .../types/domain/user/dashboard_metadata.rs | 6 + crates/router/src/types/storage/refund.rs | 75 ++++++-- .../src/utils/user/dashboard_metadata.rs | 129 ++++++++++++- crates/router_env/src/logger/types.rs | 6 + 19 files changed, 728 insertions(+), 54 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 3634b51e0cc0..ca2932725317 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -7,7 +7,8 @@ use crate::user::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, - DashboardEntryResponse, GetUsersResponse, SignUpRequest, SignUpWithMerchantIdRequest, + DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, + InviteUserResponse, ResetPasswordRequest, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, }; @@ -33,7 +34,11 @@ common_utils::impl_misc_api_event_type!( UserMerchantCreate, GetUsersResponse, AuthorizeResponse, - ConnectAccountRequest + ConnectAccountRequest, + ForgotPasswordRequest, + ResetPasswordRequest, + InviteUserRequest, + InviteUserResponse ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 6fe8be8b5291..e89de9c58934 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -174,7 +174,7 @@ pub struct RefundListMetaData { pub currency: Vec, /// The list of available refund status filters #[schema(value_type = Vec)] - pub status: Vec, + pub refund_status: Vec, } /// The status for refunds diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 287c377eb46a..e5f06fdbfae3 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -65,6 +65,29 @@ pub struct ChangePasswordRequest { pub old_password: Secret, } +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ForgotPasswordRequest { + pub email: pii::Email, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ResetPasswordRequest { + pub token: Secret, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct InviteUserRequest { + pub email: pii::Email, + pub name: Secret, + pub role_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct InviteUserResponse { + pub is_email_sent: bool, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct SwitchMerchantIdRequest { pub merchant_id: String, diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs index 04cda3bd7075..11588bbfbafe 100644 --- a/crates/api_models/src/user/dashboard_metadata.rs +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -1,3 +1,5 @@ +use common_enums::CountryAlpha2; +use common_utils::pii; use masking::Secret; use strum::EnumString; @@ -12,8 +14,11 @@ pub enum SetMetaDataRequest { ConfiguredRouting(ConfiguredRouting), TestPayment(TestPayment), IntegrationMethod(IntegrationMethod), + ConfigurationType(ConfigurationType), IntegrationCompleted, SPRoutingConfigured(ConfiguredRouting), + Feedback(Feedback), + ProdIntent(ProdIntent), SPTestPayment, DownloadWoocom, ConfigureWoocom, @@ -49,10 +54,38 @@ pub struct TestPayment { pub payment_id: String, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct IntegrationMethod { pub integration_type: String, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub enum ConfigurationType { + Single, + Multiple, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct Feedback { + pub email: pii::Email, + pub description: Option, + pub rating: Option, + pub category: Option, +} +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ProdIntent { + pub legal_business_name: Option, + pub business_label: Option, + pub business_location: Option, + pub display_name: Option, + pub poc_email: Option, + pub business_type: Option, + pub business_identifier: Option, + pub business_website: Option, + pub poc_name: Option, + pub poc_contact: Option, + pub comments: Option, + pub is_completed: bool, +} #[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] pub enum GetMetaDataRequest { @@ -65,10 +98,13 @@ pub enum GetMetaDataRequest { ConfiguredRouting, TestPayment, IntegrationMethod, + ConfigurationType, IntegrationCompleted, StripeConnected, PaypalConnected, SPRoutingConfigured, + Feedback, + ProdIntent, SPTestPayment, DownloadWoocom, ConfigureWoocom, @@ -98,10 +134,13 @@ pub enum GetMetaDataResponse { ConfiguredRouting(Option), TestPayment(Option), IntegrationMethod(Option), + ConfigurationType(Option), IntegrationCompleted(bool), StripeConnected(Option), PaypalConnected(Option), SPRoutingConfigured(Option), + Feedback(Option), + ProdIntent(Option), SPTestPayment(bool), DownloadWoocom(bool), ConfigureWoocom(bool), diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 3f8b37cd03f7..17837d2ce5c7 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -452,10 +452,13 @@ pub enum DashboardMetadata { ConfiguredRouting, TestPayment, IntegrationMethod, + ConfigurationType, IntegrationCompleted, StripeConnected, PaypalConnected, SpRoutingConfigured, + Feedback, + ProdIntent, SpTestPayment, DownloadWoocom, ConfigureWoocom, diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index 44fd24c7acf2..678bcc2fd1f6 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -28,21 +28,36 @@ impl DashboardMetadata { data_key: enums::DashboardMetadata, dashboard_metadata_update: DashboardMetadataUpdate, ) -> StorageResult { - generics::generic_update_with_unique_predicate_get_result::< - ::Table, - _, - _, - _, - >( - conn, - dsl::user_id - .eq(user_id.to_owned()) - .and(dsl::merchant_id.eq(merchant_id.to_owned())) - .and(dsl::org_id.eq(org_id.to_owned())) - .and(dsl::data_key.eq(data_key.to_owned())), - DashboardMetadataUpdateInternal::from(dashboard_metadata_update), - ) - .await + let predicate = dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::org_id.eq(org_id.to_owned())) + .and(dsl::data_key.eq(data_key.to_owned())); + + if let Some(uid) = user_id { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + predicate.and(dsl::user_id.eq(uid)), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } else { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + predicate.and(dsl::user_id.is_null()), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } } pub async fn find_user_scoped_dashboard_metadata( diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 5e580b003408..9a5308852229 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -12,8 +12,12 @@ pub enum UserErrors { InternalServerError, #[error("InvalidCredentials")] InvalidCredentials, + #[error("UserNotFound")] + UserNotFound, #[error("UserExists")] UserExists, + #[error("LinkInvalid")] + LinkInvalid, #[error("InvalidOldPassword")] InvalidOldPassword, #[error("EmailParsingError")] @@ -60,12 +64,21 @@ impl common_utils::errors::ErrorSwitch AER::Unauthorized(ApiError::new( + sub_code, + 2, + "Email doesn’t exist. Register", + None, + )), Self::UserExists => AER::BadRequest(ApiError::new( sub_code, 3, "An account already exists with this email", None, )), + Self::LinkInvalid => { + AER::Unauthorized(ApiError::new(sub_code, 4, "Invalid or expired link", None)) + } Self::InvalidOldPassword => AER::BadRequest(ApiError::new( sub_code, 6, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index c868530f81af..01947d08d1f9 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -11,7 +11,7 @@ use router_env::logger; use super::errors::{UserErrors, UserResponse}; #[cfg(feature = "email")] -use crate::services::email::types as email_types; +use crate::services::email::{types as email_types, types::EmailToken}; use crate::{ consts, db::user::UserInterface, @@ -235,8 +235,7 @@ pub async fn change_password( user.compare_password(request.old_password) .change_context(UserErrors::InvalidOldPassword)?; - let new_password_hash = - crate::utils::user::password::generate_password_hash(request.new_password)?; + let new_password_hash = utils::user::password::generate_password_hash(request.new_password)?; let _ = UserInterface::update_user_by_user_id( &*state.store, @@ -253,6 +252,182 @@ pub async fn change_password( Ok(ApplicationResponse::StatusOk) } +#[cfg(feature = "email")] +pub async fn forgot_password( + state: AppState, + request: user_api::ForgotPasswordRequest, +) -> UserResponse<()> { + let user_email = domain::UserEmail::from_pii_email(request.email)?; + + let user_from_db = state + .store + .find_user_by_email(user_email.get_secret().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .map(domain::UserFromStorage::from)?; + + let email_contents = email_types::ResetPassword { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + user_name: domain::UserName::new(user_from_db.get_name())?, + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .map_err(|e| e.change_context(UserErrors::InternalServerError))?; + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "email")] +pub async fn reset_password( + state: AppState, + request: user_api::ResetPasswordRequest, +) -> UserResponse<()> { + let token = auth::decode_jwt::(request.token.expose().as_str(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + let password = domain::UserPassword::new(request.password)?; + + let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; + + //TODO: Create Update by email query + let user_id = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)? + .user_id; + + state + .store + .update_user_by_user_id( + user_id.as_str(), + storage_user::UserUpdate::AccountUpdate { + name: None, + password: Some(hash_password), + is_verified: Some(true), + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + //TODO: Update User role status for invited user + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "email")] +pub async fn invite_user( + state: AppState, + request: user_api::InviteUserRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse { + let inviter_user = state + .store + .find_user_by_id(user_from_token.user_id.as_str()) + .await + .change_context(UserErrors::InternalServerError)?; + + if inviter_user.email == request.email { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User Inviting themself"); + } + + utils::user_role::validate_role_id(request.role_id.as_str())?; + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + + let invitee_user = state + .store + .find_user_by_email(invitee_email.clone().get_secret().expose().as_str()) + .await; + + if let Ok(invitee_user) = invitee_user { + let invitee_user_from_db = domain::UserFromStorage::from(invitee_user); + + let now = common_utils::date_time::now(); + use diesel_models::user_role::UserRoleNew; + state + .store + .insert_user_role(UserRoleNew { + user_id: invitee_user_from_db.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id, + role_id: request.role_id, + org_id: user_from_token.org_id, + status: UserStatus::Active, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id, + created_at: now, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent: false, + })) + } else if invitee_user + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + let new_user = domain::NewUser::try_from((request.clone(), user_from_token))?; + + new_user + .insert_user_in_db(state.store.as_ref()) + .await + .change_context(UserErrors::InternalServerError)?; + new_user + .clone() + .insert_user_role_in_db(state.clone(), request.role_id, UserStatus::InvitationSent) + .await + .change_context(UserErrors::InternalServerError)?; + + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent: send_email_result.is_ok(), + })) + } else { + Err(UserErrors::InternalServerError.into()) + } +} + pub async fn create_internal_user( state: AppState, request: user_api::CreateInternalUserRequest, diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index de385fb8ed65..b537aa3ec732 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -81,12 +81,17 @@ fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult { Ok(types::MetaData::IntegrationMethod(req)) } + api::SetMetaDataRequest::ConfigurationType(req) => { + Ok(types::MetaData::ConfigurationType(req)) + } api::SetMetaDataRequest::IntegrationCompleted => { Ok(types::MetaData::IntegrationCompleted(true)) } api::SetMetaDataRequest::SPRoutingConfigured(req) => { Ok(types::MetaData::SPRoutingConfigured(req)) } + api::SetMetaDataRequest::Feedback(req) => Ok(types::MetaData::Feedback(req)), + api::SetMetaDataRequest::ProdIntent(req) => Ok(types::MetaData::ProdIntent(req)), api::SetMetaDataRequest::SPTestPayment => Ok(types::MetaData::SPTestPayment(true)), api::SetMetaDataRequest::DownloadWoocom => Ok(types::MetaData::DownloadWoocom(true)), api::SetMetaDataRequest::ConfigureWoocom => Ok(types::MetaData::ConfigureWoocom(true)), @@ -110,10 +115,13 @@ fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum { api::GetMetaDataRequest::ConfiguredRouting => DBEnum::ConfiguredRouting, api::GetMetaDataRequest::TestPayment => DBEnum::TestPayment, api::GetMetaDataRequest::IntegrationMethod => DBEnum::IntegrationMethod, + api::GetMetaDataRequest::ConfigurationType => DBEnum::ConfigurationType, api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::Feedback => DBEnum::Feedback, + api::GetMetaDataRequest::ProdIntent => DBEnum::ProdIntent, api::GetMetaDataRequest::SPTestPayment => DBEnum::SpTestPayment, api::GetMetaDataRequest::DownloadWoocom => DBEnum::DownloadWoocom, api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom, @@ -158,6 +166,10 @@ fn into_response( let resp = utils::deserialize_to_response(data)?; Ok(api::GetMetaDataResponse::IntegrationMethod(resp)) } + DBEnum::ConfigurationType => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfigurationType(resp)) + } DBEnum::IntegrationCompleted => Ok(api::GetMetaDataResponse::IntegrationCompleted( data.is_some(), )), @@ -173,6 +185,14 @@ fn into_response( let resp = utils::deserialize_to_response(data)?; Ok(api::GetMetaDataResponse::SPRoutingConfigured(resp)) } + DBEnum::Feedback => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::Feedback(resp)) + } + DBEnum::ProdIntent => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ProdIntent(resp)) + } DBEnum::SpTestPayment => Ok(api::GetMetaDataResponse::SPTestPayment(data.is_some())), DBEnum::DownloadWoocom => Ok(api::GetMetaDataResponse::DownloadWoocom(data.is_some())), DBEnum::ConfigureWoocom => Ok(api::GetMetaDataResponse::ConfigureWoocom(data.is_some())), @@ -282,15 +302,54 @@ async fn insert_metadata( .await } types::MetaData::IntegrationMethod(data) => { - utils::insert_merchant_scoped_metadata_to_db( + let mut metadata = utils::insert_merchant_scoped_metadata_to_db( state, - user.user_id, - user.merchant_id, - user.org_id, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), metadata_key, - data, + data.clone(), ) - .await + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_merchant_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + types::MetaData::ConfigurationType(data) => { + let mut metadata = utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_merchant_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata } types::MetaData::IntegrationCompleted(data) => { utils::insert_merchant_scoped_metadata_to_db( @@ -336,6 +395,56 @@ async fn insert_metadata( ) .await } + types::MetaData::Feedback(data) => { + let mut metadata = utils::insert_user_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_user_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + types::MetaData::ProdIntent(data) => { + let mut metadata = utils::insert_user_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_user_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } types::MetaData::SPTestPayment(data) => { utils::insert_merchant_scoped_metadata_to_db( state, @@ -400,7 +509,8 @@ async fn fetch_metadata( metadata_keys: Vec, ) -> UserResult> { let mut dashboard_metadata = Vec::with_capacity(metadata_keys.len()); - let (merchant_scoped_enums, _) = utils::separate_metadata_type_based_on_scope(metadata_keys); + let (merchant_scoped_enums, user_scoped_enums) = + utils::separate_metadata_type_based_on_scope(metadata_keys); if !merchant_scoped_enums.is_empty() { let mut res = utils::get_merchant_scoped_metadata_from_db( @@ -413,6 +523,18 @@ async fn fetch_metadata( dashboard_metadata.append(&mut res); } + if !user_scoped_enums.is_empty() { + let mut res = utils::get_user_scoped_metadata_from_db( + state, + user.user_id.to_owned(), + user.merchant_id.to_owned(), + user.org_id.to_owned(), + user_scoped_enums, + ) + .await?; + dashboard_metadata.append(&mut res); + } + Ok(dashboard_metadata) } diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index f385e1bc5a83..1ab5a8360812 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -997,7 +997,7 @@ impl RefundInterface for MockDb { let mut refund_meta_data = api_models::refunds::RefundListMetaData { connector: vec![], currency: vec![], - status: vec![], + refund_status: vec![], }; let mut unique_connectors = HashSet::new(); @@ -1016,7 +1016,7 @@ impl RefundInterface for MockDb { refund_meta_data.connector = unique_connectors.into_iter().collect(); refund_meta_data.currency = unique_currencies.into_iter().collect(); - refund_meta_data.status = unique_statuses.into_iter().collect(); + refund_meta_data.refund_status = unique_statuses.into_iter().collect(); Ok(refund_meta_data) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5d14d1219d32..acf98c658a7c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -860,6 +860,9 @@ impl User { .service( web::resource("/connect_account").route(web::post().to(user_connect_account)), ) + .service(web::resource("/forgot_password").route(web::post().to(forgot_password))) + .service(web::resource("/reset_password").route(web::post().to(reset_password))) + .service(web::resource("user/invite").route(web::post().to(invite_user))) .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 3592506f522b..0c850922fff4 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -163,6 +163,9 @@ impl From for ApiIdentifier { | Flow::DeleteSampleData | Flow::UserMerchantAccountList | Flow::GetUserDetails + | Flow::ForgotPassword + | Flow::ResetPassword + | Flow::InviteUser | Flow::UserSignUpWithMerchantId => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 45fa0ba35c59..c4476d6ed710 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -294,3 +294,60 @@ pub async fn get_user_details(state: web::Data, req: HttpRequest) -> H )) .await } + +#[cfg(feature = "email")] +pub async fn forgot_password( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ForgotPassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, payload| user_core::forgot_password(state, payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn reset_password( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ResetPassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, payload| user_core::reset_password(state, payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn invite_user( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::InviteUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, user, payload| user_core::invite_user(state, payload, user), + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index a4a4681c6001..ad91edd8c364 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -66,6 +66,10 @@ impl EmailToken { }; jwt::generate_jwt(&token_payload, settings).await } + + pub fn get_email(&self) -> &str { + self.email.as_str() + } } pub fn get_link_with_token( diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 592195922493..16a00f117034 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -259,6 +259,15 @@ impl From for NewUserOrganization { } } +type InviteeUserRequestWithInvitedUserToken = (user_api::InviteUserRequest, UserFromToken); +impl From for NewUserOrganization { + fn from(_value: InviteeUserRequestWithInvitedUserToken) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + #[derive(Clone)] pub struct MerchantId(String); @@ -420,6 +429,19 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult { + let merchant_id = MerchantId::new(value.clone().1.merchant_id)?; + let new_organization = NewUserOrganization::from(value); + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + type UserMerchantCreateRequestWithToken = (UserFromStorage, user_api::UserMerchantCreate, UserFromToken); @@ -657,6 +679,26 @@ impl TryFrom for NewUser { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.0.email.clone().try_into()?; + let name = UserName::new(value.0.name.clone())?; + let password = password::generate_password_hash(uuid::Uuid::new_v4().to_string().into())?; + let password = UserPassword::new(password)?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + #[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs index e65379346ac9..5e4017a3cb1a 100644 --- a/crates/router/src/types/domain/user/dashboard_metadata.rs +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -13,10 +13,13 @@ pub enum MetaData { ConfiguredRouting(api::ConfiguredRouting), TestPayment(api::TestPayment), IntegrationMethod(api::IntegrationMethod), + ConfigurationType(api::ConfigurationType), IntegrationCompleted(bool), StripeConnected(api::ProcessorConnected), PaypalConnected(api::ProcessorConnected), SPRoutingConfigured(api::ConfiguredRouting), + Feedback(api::Feedback), + ProdIntent(api::ProdIntent), SPTestPayment(bool), DownloadWoocom(bool), ConfigureWoocom(bool), @@ -36,10 +39,13 @@ impl From<&MetaData> for DBEnum { MetaData::ConfiguredRouting(_) => Self::ConfiguredRouting, MetaData::TestPayment(_) => Self::TestPayment, MetaData::IntegrationMethod(_) => Self::IntegrationMethod, + MetaData::ConfigurationType(_) => Self::ConfigurationType, MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, MetaData::StripeConnected(_) => Self::StripeConnected, MetaData::PaypalConnected(_) => Self::PaypalConnected, MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::Feedback(_) => Self::Feedback, + MetaData::ProdIntent(_) => Self::ProdIntent, MetaData::SPTestPayment(_) => Self::SpTestPayment, MetaData::DownloadWoocom(_) => Self::DownloadWoocom, MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom, diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index 4d5667700122..bb05233173c8 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -50,23 +50,40 @@ impl RefundDbExt for Refund { .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .order(dsl::modified_at.desc()) .into_boxed(); - - match &refund_list_details.payment_id { - Some(pid) => { - filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); - } - None => { - filter = filter.limit(limit).offset(offset); - } - }; - match &refund_list_details.refund_id { - Some(ref_id) => { - filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); - } - None => { - filter = filter.limit(limit).offset(offset); - } + let mut search_by_pay_or_ref_id = false; + + if let (Some(pid), Some(ref_id)) = ( + &refund_list_details.payment_id, + &refund_list_details.refund_id, + ) { + search_by_pay_or_ref_id = true; + filter = filter + .filter(dsl::payment_id.eq(pid.to_owned())) + .or_filter(dsl::refund_id.eq(ref_id.to_owned())) + .limit(limit) + .offset(offset); }; + + if !search_by_pay_or_ref_id { + match &refund_list_details.payment_id { + Some(pid) => { + filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); + } + None => { + filter = filter.limit(limit).offset(offset); + } + }; + } + if !search_by_pay_or_ref_id { + match &refund_list_details.refund_id { + Some(ref_id) => { + filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + } + None => { + filter = filter.limit(limit).offset(offset); + } + }; + } match &refund_list_details.profile_id { Some(profile_id) => { filter = filter @@ -163,7 +180,7 @@ impl RefundDbExt for Refund { let meta = api_models::refunds::RefundListMetaData { connector: filter_connector, currency: filter_currency, - status: filter_status, + refund_status: filter_status, }; Ok(meta) @@ -179,12 +196,28 @@ impl RefundDbExt for Refund { .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .into_boxed(); - if let Some(pay_id) = &refund_list_details.payment_id { - filter = filter.filter(dsl::payment_id.eq(pay_id.to_owned())); + let mut search_by_pay_or_ref_id = false; + + if let (Some(pid), Some(ref_id)) = ( + &refund_list_details.payment_id, + &refund_list_details.refund_id, + ) { + search_by_pay_or_ref_id = true; + filter = filter + .filter(dsl::payment_id.eq(pid.to_owned())) + .or_filter(dsl::refund_id.eq(ref_id.to_owned())); + }; + + if !search_by_pay_or_ref_id { + if let Some(pay_id) = &refund_list_details.payment_id { + filter = filter.filter(dsl::payment_id.eq(pay_id.to_owned())); + } } - if let Some(ref_id) = &refund_list_details.refund_id { - filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + if !search_by_pay_or_ref_id { + if let Some(ref_id) = &refund_list_details.refund_id { + filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + } } if let Some(profile_id) = &refund_list_details.profile_id { filter = filter.filter(dsl::profile_id.eq(profile_id.to_owned())); diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs index 5f354e613f95..40594a6e49f6 100644 --- a/crates/router/src/utils/user/dashboard_metadata.rs +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -6,7 +6,7 @@ use api_models::user::dashboard_metadata::{ }; use diesel_models::{ enums::DashboardMetadata as DBEnum, - user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate}, }; use error_stack::{IntoReport, ResultExt}; use masking::Secret; @@ -50,6 +50,40 @@ pub async fn insert_merchant_scoped_metadata_to_db( e.change_context(UserErrors::InternalServerError) }) } +pub async fn insert_user_scoped_metadata_to_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let now = common_utils::date_time::now(); + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + state + .store + .insert_metadata(DashboardMetadataNew { + user_id: Some(user_id.clone()), + merchant_id, + org_id, + data_key: metadata_key, + data_value, + created_by: user_id.clone(), + created_at: now, + last_modified_by: user_id, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + return e.change_context(UserErrors::MetadataAlreadySet); + } + e.change_context(UserErrors::InternalServerError) + }) +} pub async fn get_merchant_scoped_metadata_from_db( state: &AppState, @@ -73,6 +107,88 @@ pub async fn get_merchant_scoped_metadata_from_db( } } } +pub async fn get_user_scoped_metadata_from_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_user_scoped_dashboard_metadata(&user_id, &merchant_id, &org_id, metadata_keys) + .await + { + Ok(data) => Ok(data), + Err(e) => { + if e.current_context().is_db_not_found() { + return Ok(Vec::with_capacity(0)); + } + Err(e + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData")) + } + } +} + +pub async fn update_merchant_scoped_metadata( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + + state + .store + .update_metadata( + None, + merchant_id, + org_id, + metadata_key, + DashboardMetadataUpdate::UpdateData { + data_key: metadata_key, + data_value, + last_modified_by: user_id, + }, + ) + .await + .change_context(UserErrors::InternalServerError) +} +pub async fn update_user_scoped_metadata( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + + state + .store + .update_metadata( + Some(user_id.clone()), + merchant_id, + org_id, + metadata_key, + DashboardMetadataUpdate::UpdateData { + data_key: metadata_key, + data_value, + last_modified_by: user_id, + }, + ) + .await + .change_context(UserErrors::InternalServerError) +} pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResult> where @@ -87,7 +203,7 @@ where pub fn separate_metadata_type_based_on_scope( metadata_keys: Vec, ) -> (Vec, Vec) { - let (mut merchant_scoped, user_scoped) = ( + let (mut merchant_scoped, mut user_scoped) = ( Vec::with_capacity(metadata_keys.len()), Vec::with_capacity(metadata_keys.len()), ); @@ -102,6 +218,7 @@ pub fn separate_metadata_type_based_on_scope( | DBEnum::ConfiguredRouting | DBEnum::TestPayment | DBEnum::IntegrationMethod + | DBEnum::ConfigurationType | DBEnum::IntegrationCompleted | DBEnum::StripeConnected | DBEnum::PaypalConnected @@ -111,11 +228,19 @@ pub fn separate_metadata_type_based_on_scope( | DBEnum::ConfigureWoocom | DBEnum::SetupWoocomWebhook | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + DBEnum::Feedback | DBEnum::ProdIntent => user_scoped.push(key), } } (merchant_scoped, user_scoped) } +pub fn is_update_required(metadata: &UserResult) -> bool { + match metadata { + Ok(_) => false, + Err(e) => matches!(e.current_context(), UserErrors::MetadataAlreadySet), + } +} + pub fn is_backfill_required(metadata_key: &DBEnum) -> bool { matches!( metadata_key, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b4e530692319..d35090551de7 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -293,6 +293,12 @@ pub enum Flow { UserMerchantAccountList, /// Get users for merchant account GetUserDetails, + /// Get reset password link + ForgotPassword, + /// Reset password using link + ResetPassword, + /// Invite users + InviteUser, /// Incremental Authorization flow PaymentsIncrementalAuthorization, } From 8b7a7aa6494ff669e1f8bcc92a5160e422d6b26e Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:44:24 +0530 Subject: [PATCH 146/443] docs(test_utils): Update postman docs (#3055) --- crates/test_utils/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/test_utils/README.md b/crates/test_utils/README.md index 2edbc7104c25..a82c74cb59f6 100644 --- a/crates/test_utils/README.md +++ b/crates/test_utils/README.md @@ -22,9 +22,9 @@ The heart of `newman`(with directory support) and `UI-tests` Required fields: -- `--admin_api_key` -- Admin API Key of the environment. `test_admin` is the Admin API Key for running locally -- `--base_url` -- Base URL of the environment. `http://127.0.0.1:8080` / `http://localhost:8080` is the Base URL for running locally -- `--connector_name` -- Name of the connector that you wish to run. Example: `adyen`, `shift4`, `stripe` +- `--admin-api-key` -- Admin API Key of the environment. `test_admin` is the Admin API Key for running locally +- `--base-url` -- Base URL of the environment. `http://127.0.0.1:8080` / `http://localhost:8080` is the Base URL for running locally +- `--connector-name` -- Name of the connector that you wish to run. Example: `adyen`, `shift4`, `stripe` Optional fields: @@ -46,7 +46,7 @@ Optional fields: - Tests can be run with the following command: ```shell - cargo run --package test_utils --bin test_utils -- --connector_name= --base_url= --admin_api_key= \ + cargo run --package test_utils --bin test_utils -- --connector-name= --base-url= --admin-api-key= \ # optionally --folder ",,..." --verbose ``` From 53df543b7f1407a758232025b7de0fb527be8e86 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:03:38 +0530 Subject: [PATCH 147/443] fix: remove redundant call to populate_payment_data function (#3054) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- Cargo.lock | 1 - crates/common_utils/Cargo.toml | 1 - crates/router/src/core/payments.rs | 4 ---- 3 files changed, 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb38c0b70b59..d2e8d9dd5df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,7 +1708,6 @@ dependencies = [ "thiserror", "time", "tokio 1.32.0", - "utoipa", ] [[package]] diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3a41b111b39d..3619c93d772c 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -38,7 +38,6 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } -utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 16fda276f6a5..21a2866c9f4e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -953,10 +953,6 @@ where payment_data, ) .await?; - operation - .to_domain()? - .populate_payment_data(state, payment_data, merchant_account) - .await?; let mut router_data = payment_data .construct_router_data( From 7bd6e05c0c05ebae9b82a6f410e61ca4409d088b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:29:10 +0530 Subject: [PATCH 148/443] feat(connector_onboarding): Add Connector onboarding APIs (#3050) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 6 + config/development.toml | 8 +- config/docker_compose.toml | 6 + crates/api_models/src/connector_onboarding.rs | 54 ++++ crates/api_models/src/events.rs | 1 + .../src/events/connector_onboarding.rs | 12 + crates/api_models/src/lib.rs | 1 + crates/router/src/configs/kms.rs | 33 +++ crates/router/src/configs/settings.rs | 17 ++ crates/router/src/core.rs | 2 + .../router/src/core/connector_onboarding.rs | 96 +++++++ .../src/core/connector_onboarding/paypal.rs | 174 ++++++++++++ crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 8 +- crates/router/src/routes/app.rs | 26 +- .../router/src/routes/connector_onboarding.rs | 47 ++++ crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/types/api.rs | 2 + .../src/types/api/connector_onboarding.rs | 1 + .../types/api/connector_onboarding/paypal.rs | 247 ++++++++++++++++++ crates/router/src/utils.rs | 2 + .../router/src/utils/connector_onboarding.rs | 36 +++ .../src/utils/connector_onboarding/paypal.rs | 89 +++++++ crates/router_env/src/logger/types.rs | 4 + loadtest/config/development.toml | 6 + 25 files changed, 876 insertions(+), 6 deletions(-) create mode 100644 crates/api_models/src/connector_onboarding.rs create mode 100644 crates/api_models/src/events/connector_onboarding.rs create mode 100644 crates/router/src/core/connector_onboarding.rs create mode 100644 crates/router/src/core/connector_onboarding/paypal.rs create mode 100644 crates/router/src/routes/connector_onboarding.rs create mode 100644 crates/router/src/types/api/connector_onboarding.rs create mode 100644 crates/router/src/types/api/connector_onboarding/paypal.rs create mode 100644 crates/router/src/utils/connector_onboarding.rs create mode 100644 crates/router/src/utils/connector_onboarding/paypal.rs diff --git a/config/config.example.toml b/config/config.example.toml index d935a4e7f20d..fad4da3e7c36 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -477,3 +477,9 @@ connection_timeout = 10 # Timeout for database connection in seconds [kv_config] # TTL for KV in seconds ttl = 900 + +[paypal_onboarding] +client_id = "paypal_client_id" # Client ID for PayPal onboarding +client_secret = "paypal_secret_key" # Secret key for PayPal onboarding +partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding +enabled = true # Switch to enable or disable PayPal onboarding diff --git a/config/development.toml b/config/development.toml index fa5fddb0d60a..2eb8b00b9c08 100644 --- a/config/development.toml +++ b/config/development.toml @@ -504,4 +504,10 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 -queue_strategy = "Fifo" \ No newline at end of file +queue_strategy = "Fifo" + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 4d50600e1bf8..de90f3c70abd 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -362,3 +362,9 @@ queue_strategy = "Fifo" [kv_config] ttl = 900 # 15 * 60 seconds + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true diff --git a/crates/api_models/src/connector_onboarding.rs b/crates/api_models/src/connector_onboarding.rs new file mode 100644 index 000000000000..759d3cb97f13 --- /dev/null +++ b/crates/api_models/src/connector_onboarding.rs @@ -0,0 +1,54 @@ +use super::{admin, enums}; + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct ActionUrlRequest { + pub connector: enums::Connector, + pub connector_id: String, + pub return_url: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ActionUrlResponse { + PayPal(PayPalActionUrlResponse), +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct OnboardingSyncRequest { + pub profile_id: String, + pub connector_id: String, + pub connector: enums::Connector, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalActionUrlResponse { + pub action_url: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum OnboardingStatus { + PayPal(PayPalOnboardingStatus), +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PayPalOnboardingStatus { + AccountNotFound, + PaymentsNotReceivable, + PpcpCustomDenied, + MorePermissionsNeeded, + EmailNotVerified, + Success(PayPalOnboardingDone), + ConnectorIntegrated(admin::MerchantConnectorResponse), +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalOnboardingDone { + pub payer_id: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalIntegrationDone { + pub connector_id: String, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index ac7cdeb83d94..457d3fde05b7 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,3 +1,4 @@ +pub mod connector_onboarding; pub mod customer; pub mod gsm; mod locker_migration; diff --git a/crates/api_models/src/events/connector_onboarding.rs b/crates/api_models/src/events/connector_onboarding.rs new file mode 100644 index 000000000000..998dc384d620 --- /dev/null +++ b/crates/api_models/src/events/connector_onboarding.rs @@ -0,0 +1,12 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::connector_onboarding::{ + ActionUrlRequest, ActionUrlResponse, OnboardingStatus, OnboardingSyncRequest, +}; + +common_utils::impl_misc_api_event_type!( + ActionUrlRequest, + ActionUrlResponse, + OnboardingSyncRequest, + OnboardingStatus +); diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 056888839a54..ce3c11d9c2f3 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod api_keys; pub mod bank_accounts; pub mod cards_info; pub mod conditional_configs; +pub mod connector_onboarding; pub mod currency; pub mod customers; pub mod disputes; diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 37f2d15774a5..bf6ee44d28be 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -69,3 +69,36 @@ impl KmsDecrypt for settings::Database { }) } } + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl KmsDecrypt for settings::PayPalOnboarding { + type Output = Self; + + async fn decrypt_inner( + mut self, + kms_client: &KmsClient, + ) -> CustomResult { + self.client_id = kms_client.decrypt(self.client_id.expose()).await?.into(); + self.client_secret = kms_client + .decrypt(self.client_secret.expose()) + .await? + .into(); + self.partner_id = kms_client.decrypt(self.partner_id.expose()).await?.into(); + Ok(self) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl KmsDecrypt for settings::ConnectorOnboarding { + type Output = Self; + + async fn decrypt_inner( + mut self, + kms_client: &KmsClient, + ) -> CustomResult { + self.paypal = self.paypal.decrypt_inner(kms_client).await?; + Ok(self) + } +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f2d962b0abee..68af91d06612 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -116,6 +116,8 @@ pub struct Settings { #[cfg(feature = "olap")] pub report_download_config: ReportConfig, pub events: EventsConfig, + #[cfg(feature = "olap")] + pub connector_onboarding: ConnectorOnboarding, } #[derive(Debug, Deserialize, Clone)] @@ -884,3 +886,18 @@ impl<'de> Deserialize<'de> for LockSettings { }) } } + +#[cfg(feature = "olap")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ConnectorOnboarding { + pub paypal: PayPalOnboarding, +} + +#[cfg(feature = "olap")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct PayPalOnboarding { + pub client_id: masking::Secret, + pub client_secret: masking::Secret, + pub partner_id: masking::Secret, + pub enabled: bool, +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 08de9cf80384..6a167be48dae 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -5,6 +5,8 @@ pub mod cache; pub mod cards_info; pub mod conditional_config; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; #[cfg(any(feature = "olap", feature = "oltp"))] pub mod currency; pub mod customers; diff --git a/crates/router/src/core/connector_onboarding.rs b/crates/router/src/core/connector_onboarding.rs new file mode 100644 index 000000000000..e48026edc2d5 --- /dev/null +++ b/crates/router/src/core/connector_onboarding.rs @@ -0,0 +1,96 @@ +use api_models::{connector_onboarding as api, enums}; +use error_stack::ResultExt; +use masking::Secret; + +use crate::{ + core::errors::{ApiErrorResponse, RouterResponse, RouterResult}, + services::{authentication as auth, ApplicationResponse}, + types::{self as oss_types}, + utils::connector_onboarding as utils, + AppState, +}; + +pub mod paypal; + +#[async_trait::async_trait] +pub trait AccessToken { + async fn access_token(state: &AppState) -> RouterResult; +} + +pub async fn get_action_url( + state: AppState, + request: api::ActionUrlRequest, +) -> RouterResponse { + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + + match (is_enabled, request.connector) { + (Some(true), enums::Connector::Paypal) => { + let action_url = Box::pin(paypal::get_action_url_from_paypal( + state, + request.connector_id, + request.return_url, + )) + .await?; + Ok(ApplicationResponse::Json(api::ActionUrlResponse::PayPal( + api::PayPalActionUrlResponse { action_url }, + ))) + } + _ => Err(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: request.connector.to_string(), + } + .into()), + } +} + +pub async fn sync_onboarding_status( + state: AppState, + user_from_token: auth::UserFromToken, + request: api::OnboardingSyncRequest, +) -> RouterResponse { + let merchant_account = user_from_token + .get_merchant_account(state.clone()) + .await + .change_context(ApiErrorResponse::MerchantAccountNotFound)?; + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + + match (is_enabled, request.connector) { + (Some(true), enums::Connector::Paypal) => { + let status = Box::pin(paypal::sync_merchant_onboarding_status( + state.clone(), + request.connector_id.clone(), + )) + .await?; + if let api::OnboardingStatus::PayPal(api::PayPalOnboardingStatus::Success( + ref inner_data, + )) = status + { + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let auth_details = oss_types::ConnectorAuthType::SignatureKey { + api_key: connector_onboarding_conf.paypal.client_secret, + key1: connector_onboarding_conf.paypal.client_id, + api_secret: Secret::new(inner_data.payer_id.clone()), + }; + let some_data = paypal::update_mca( + &state, + &merchant_account, + request.connector_id.to_owned(), + auth_details, + ) + .await?; + + return Ok(ApplicationResponse::Json(api::OnboardingStatus::PayPal( + api::PayPalOnboardingStatus::ConnectorIntegrated(some_data), + ))); + } + Ok(ApplicationResponse::Json(status)) + } + _ => Err(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: request.connector.to_string(), + } + .into()), + } +} diff --git a/crates/router/src/core/connector_onboarding/paypal.rs b/crates/router/src/core/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..30aa69067b5d --- /dev/null +++ b/crates/router/src/core/connector_onboarding/paypal.rs @@ -0,0 +1,174 @@ +use api_models::{admin::MerchantConnectorUpdate, connector_onboarding as api}; +use common_utils::ext_traits::Encode; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface, Secret}; + +use crate::{ + core::{ + admin, + errors::{ApiErrorResponse, RouterResult}, + }, + services::{send_request, ApplicationResponse, Request}, + types::{self as oss_types, api as oss_api_types, api::connector_onboarding as types}, + utils::connector_onboarding as utils, + AppState, +}; + +fn build_referral_url(state: AppState) -> String { + format!( + "{}v2/customer/partner-referrals", + state.conf.connectors.paypal.base_url + ) +} + +async fn build_referral_request( + state: AppState, + connector_id: String, + return_url: String, +) -> RouterResult { + let access_token = utils::paypal::generate_access_token(state.clone()).await?; + let request_body = types::paypal::PartnerReferralRequest::new(connector_id, return_url); + + utils::paypal::build_paypal_post_request( + build_referral_url(state), + request_body, + access_token.token.expose(), + ) +} + +pub async fn get_action_url_from_paypal( + state: AppState, + connector_id: String, + return_url: String, +) -> RouterResult { + let referral_request = Box::pin(build_referral_request( + state.clone(), + connector_id, + return_url, + )) + .await?; + let referral_response = send_request(&state, referral_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal referrals")?; + + let parsed_response: types::paypal::PartnerReferralResponse = referral_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal response")?; + + parsed_response.extract_action_url() +} + +fn merchant_onboarding_status_url(state: AppState, tracking_id: String) -> String { + let partner_id = state.conf.connector_onboarding.paypal.partner_id.to_owned(); + format!( + "{}v1/customer/partners/{}/merchant-integrations?tracking_id={}", + state.conf.connectors.paypal.base_url, + partner_id.expose(), + tracking_id + ) +} + +pub async fn sync_merchant_onboarding_status( + state: AppState, + tracking_id: String, +) -> RouterResult { + let access_token = utils::paypal::generate_access_token(state.clone()).await?; + + let Some(seller_status_response) = + find_paypal_merchant_by_tracking_id(state.clone(), tracking_id, &access_token).await? + else { + return Ok(api::OnboardingStatus::PayPal( + api::PayPalOnboardingStatus::AccountNotFound, + )); + }; + + let merchant_details_url = seller_status_response + .extract_merchant_details_url(&state.conf.connectors.paypal.base_url)?; + + let merchant_details_request = + utils::paypal::build_paypal_get_request(merchant_details_url, access_token.token.expose())?; + + let merchant_details_response = send_request(&state, merchant_details_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal merchant details")?; + + let parsed_response: types::paypal::SellerStatusDetailsResponse = merchant_details_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal merchant details response")?; + + let eligibity = parsed_response.get_eligibility_status().await?; + Ok(api::OnboardingStatus::PayPal(eligibity)) +} + +async fn find_paypal_merchant_by_tracking_id( + state: AppState, + tracking_id: String, + access_token: &oss_types::AccessToken, +) -> RouterResult> { + let seller_status_request = utils::paypal::build_paypal_get_request( + merchant_onboarding_status_url(state.clone(), tracking_id), + access_token.token.peek().to_string(), + )?; + let seller_status_response = send_request(&state, seller_status_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal onboarding status")?; + + if seller_status_response.status().is_success() { + return Ok(Some( + seller_status_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal onboarding status response")?, + )); + } + Ok(None) +} + +pub async fn update_mca( + state: &AppState, + merchant_account: &oss_types::domain::MerchantAccount, + connector_id: String, + auth_details: oss_types::ConnectorAuthType, +) -> RouterResult { + let connector_auth_json = + Encode::::encode_to_value(&auth_details) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error while deserializing connector_account_details")?; + + let request = MerchantConnectorUpdate { + connector_type: common_enums::ConnectorType::PaymentProcessor, + connector_account_details: Some(Secret::new(connector_auth_json)), + disabled: Some(false), + status: Some(common_enums::ConnectorStatus::Active), + test_mode: None, + connector_label: None, + payment_methods_enabled: None, + metadata: None, + frm_configs: None, + connector_webhook_details: None, + pm_auth_config: None, + }; + let mca_response = admin::update_payment_connector( + state.clone(), + &merchant_account.merchant_id, + &connector_id, + request, + ) + .await?; + + match mca_response { + ApplicationResponse::Json(mca_data) => Ok(mca_data), + _ => Err(ApiErrorResponse::InternalServerError.into()), + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index fb8be9636748..3b4c7ce9b7d3 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -147,6 +147,7 @@ pub fn mk_app( .service(routes::Gsm::server(state.clone())) .service(routes::PaymentLink::server(state.clone())) .service(routes::User::server(state.clone())) + .service(routes::ConnectorOnboarding::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index b19ef5d7016b..9b3006692d34 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -4,6 +4,8 @@ pub mod app; pub mod cache; pub mod cards_info; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; #[cfg(any(feature = "olap", feature = "oltp"))] pub mod currency; pub mod customers; @@ -47,9 +49,9 @@ pub use self::app::Routing; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ - ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount, - PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, + ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, ConnectorOnboarding, Customers, + Disputes, EphemeralKey, Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, + MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index acf98c658a7c..9739d18864b8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -26,8 +26,8 @@ use super::routing as cloud_routing; use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] use super::{ - admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, - user::*, user_role::*, + admin::*, api_keys::*, connector_onboarding::*, disputes::*, files::*, gsm::*, + locker_migration, payment_link::*, user::*, user_role::*, }; use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -185,6 +185,16 @@ impl AppState { } }; + #[cfg(all(feature = "kms", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.connector_onboarding = conf + .connector_onboarding + .decrypt_inner(kms_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(feature = "olap")] let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; @@ -888,3 +898,15 @@ impl LockerMigrate { ) } } + +pub struct ConnectorOnboarding; + +#[cfg(feature = "olap")] +impl ConnectorOnboarding { + pub fn server(state: AppState) -> Scope { + web::scope("/connector_onboarding") + .app_data(web::Data::new(state)) + .service(web::resource("/action_url").route(web::post().to(get_action_url))) + .service(web::resource("/sync").route(web::post().to(sync_onboarding_status))) + } +} diff --git a/crates/router/src/routes/connector_onboarding.rs b/crates/router/src/routes/connector_onboarding.rs new file mode 100644 index 000000000000..b7c39b3c1d2e --- /dev/null +++ b/crates/router/src/routes/connector_onboarding.rs @@ -0,0 +1,47 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::connector_onboarding as api_types; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, connector_onboarding as core}, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn get_action_url( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::GetActionUrl; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _: auth::UserFromToken, req| core::get_action_url(state, req), + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn sync_onboarding_status( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SyncOnboardingStatus; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + core::sync_onboarding_status, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 0c850922fff4..dcae11f58b76 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -28,6 +28,7 @@ pub enum ApiIdentifier { Gsm, User, UserRole, + ConnectorOnboarding, } impl From for ApiIdentifier { @@ -171,6 +172,8 @@ impl From for ApiIdentifier { Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole } + + Flow::GetActionUrl | Flow::SyncOnboardingStatus => Self::ConnectorOnboarding, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c74608ea20a1..0ec158199cea 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -1,6 +1,8 @@ pub mod admin; pub mod api_keys; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; pub mod customers; pub mod disputes; pub mod enums; diff --git a/crates/router/src/types/api/connector_onboarding.rs b/crates/router/src/types/api/connector_onboarding.rs new file mode 100644 index 000000000000..5b1d581a20ef --- /dev/null +++ b/crates/router/src/types/api/connector_onboarding.rs @@ -0,0 +1 @@ +pub mod paypal; diff --git a/crates/router/src/types/api/connector_onboarding/paypal.rs b/crates/router/src/types/api/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..0cc026d4d7ad --- /dev/null +++ b/crates/router/src/types/api/connector_onboarding/paypal.rs @@ -0,0 +1,247 @@ +use api_models::connector_onboarding as api; +use error_stack::{IntoReport, ResultExt}; + +use crate::core::errors::{ApiErrorResponse, RouterResult}; + +#[derive(serde::Deserialize, Debug)] +pub struct HateoasLink { + pub href: String, + pub rel: String, + pub method: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct PartnerReferralResponse { + pub links: Vec, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralRequest { + pub tracking_id: String, + pub operations: Vec, + pub products: Vec, + pub capabilities: Vec, + pub partner_config_override: PartnerConfigOverride, + pub legal_consents: Vec, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalProducts { + Ppcp, + AdvancedVaulting, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalCapabilities { + PaypalWalletVaultingAdvanced, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralOperations { + pub operation: PayPalReferralOperationType, + pub api_integration_preference: PartnerReferralIntegrationPreference, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalReferralOperationType { + ApiIntegration, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralIntegrationPreference { + pub rest_api_integration: PartnerReferralRestApiIntegration, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralRestApiIntegration { + pub integration_method: IntegrationMethod, + pub integration_type: PayPalIntegrationType, + pub third_party_details: PartnerReferralThirdPartyDetails, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IntegrationMethod { + Paypal, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalIntegrationType { + ThirdParty, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralThirdPartyDetails { + pub features: Vec, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalFeatures { + Payment, + Refund, + Vault, + AccessMerchantInformation, + BillingAgreement, + ReadSellerDispute, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerConfigOverride { + pub partner_logo_url: String, + pub return_url: String, +} + +#[derive(serde::Serialize, Debug)] +pub struct LegalConsent { + #[serde(rename = "type")] + pub consent_type: LegalConsentType, + pub granted: bool, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum LegalConsentType { + ShareDataConsent, +} + +impl PartnerReferralRequest { + pub fn new(tracking_id: String, return_url: String) -> Self { + Self { + tracking_id, + operations: vec![PartnerReferralOperations { + operation: PayPalReferralOperationType::ApiIntegration, + api_integration_preference: PartnerReferralIntegrationPreference { + rest_api_integration: PartnerReferralRestApiIntegration { + integration_method: IntegrationMethod::Paypal, + integration_type: PayPalIntegrationType::ThirdParty, + third_party_details: PartnerReferralThirdPartyDetails { + features: vec![ + PayPalFeatures::Payment, + PayPalFeatures::Refund, + PayPalFeatures::Vault, + PayPalFeatures::AccessMerchantInformation, + PayPalFeatures::BillingAgreement, + PayPalFeatures::ReadSellerDispute, + ], + }, + }, + }, + }], + products: vec![PayPalProducts::Ppcp, PayPalProducts::AdvancedVaulting], + capabilities: vec![PayPalCapabilities::PaypalWalletVaultingAdvanced], + partner_config_override: PartnerConfigOverride { + partner_logo_url: "https://hyperswitch.io/img/websiteIcon.svg".to_string(), + return_url, + }, + legal_consents: vec![LegalConsent { + consent_type: LegalConsentType::ShareDataConsent, + granted: true, + }], + } + } +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusResponse { + pub merchant_id: String, + pub links: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusDetailsResponse { + pub merchant_id: String, + pub primary_email_confirmed: bool, + pub payments_receivable: bool, + pub products: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusProducts { + pub name: String, + pub vetting_status: Option, +} + +#[derive(serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VettingStatus { + NeedMoreData, + Subscribed, + Denied, +} + +impl SellerStatusResponse { + pub fn extract_merchant_details_url(self, paypal_base_url: &str) -> RouterResult { + self.links + .get(0) + .and_then(|link| link.href.strip_prefix('/')) + .map(|link| format!("{}{}", paypal_base_url, link)) + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Merchant details not received in onboarding status") + } +} + +impl SellerStatusDetailsResponse { + pub fn check_payments_receivable(&self) -> Option { + if !self.payments_receivable { + return Some(api::PayPalOnboardingStatus::PaymentsNotReceivable); + } + None + } + + pub fn check_ppcp_custom_status(&self) -> Option { + match self.get_ppcp_custom_status() { + Some(VettingStatus::Denied) => Some(api::PayPalOnboardingStatus::PpcpCustomDenied), + Some(VettingStatus::Subscribed) => None, + _ => Some(api::PayPalOnboardingStatus::MorePermissionsNeeded), + } + } + + fn check_email_confirmation(&self) -> Option { + if !self.primary_email_confirmed { + return Some(api::PayPalOnboardingStatus::EmailNotVerified); + } + None + } + + pub async fn get_eligibility_status(&self) -> RouterResult { + Ok(self + .check_payments_receivable() + .or(self.check_email_confirmation()) + .or(self.check_ppcp_custom_status()) + .unwrap_or(api::PayPalOnboardingStatus::Success( + api::PayPalOnboardingDone { + payer_id: self.get_payer_id(), + }, + ))) + } + + fn get_ppcp_custom_status(&self) -> Option { + self.products + .iter() + .find(|product| product.name == "PPCP_CUSTOM") + .and_then(|ppcp_custom| ppcp_custom.vetting_status.clone()) + } + + fn get_payer_id(&self) -> String { + self.merchant_id.to_string() + } +} + +impl PartnerReferralResponse { + pub fn extract_action_url(self) -> RouterResult { + Ok(self + .links + .into_iter() + .find(|hateoas_link| hateoas_link.rel == "action_url") + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Failed to get action_url from paypal response")? + .href) + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index f1590342e17c..42116e1ecbf0 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "olap")] +pub mod connector_onboarding; pub mod currency; pub mod custom_serde; pub mod db_utils; diff --git a/crates/router/src/utils/connector_onboarding.rs b/crates/router/src/utils/connector_onboarding.rs new file mode 100644 index 000000000000..e8afcd68a468 --- /dev/null +++ b/crates/router/src/utils/connector_onboarding.rs @@ -0,0 +1,36 @@ +use crate::{ + core::errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, + routes::app::settings, + types::{self, api::enums}, +}; + +pub mod paypal; + +pub fn get_connector_auth( + connector: enums::Connector, + connector_data: &settings::ConnectorOnboarding, +) -> RouterResult { + match connector { + enums::Connector::Paypal => Ok(types::ConnectorAuthType::BodyKey { + api_key: connector_data.paypal.client_secret.clone(), + key1: connector_data.paypal.client_id.clone(), + }), + _ => Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason(format!( + "Onboarding is not implemented for {}", + connector + )), + } + .into()), + } +} + +pub fn is_enabled( + connector: types::Connector, + conf: &settings::ConnectorOnboarding, +) -> Option { + match connector { + enums::Connector::Paypal => Some(conf.paypal.enabled), + _ => None, + } +} diff --git a/crates/router/src/utils/connector_onboarding/paypal.rs b/crates/router/src/utils/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..c803775be071 --- /dev/null +++ b/crates/router/src/utils/connector_onboarding/paypal.rs @@ -0,0 +1,89 @@ +use common_utils::{ + ext_traits::Encode, + request::{Method, Request, RequestBuilder}, +}; +use error_stack::{IntoReport, ResultExt}; +use http::header; +use serde_json::json; + +use crate::{ + connector, + core::errors::{ApiErrorResponse, RouterResult}, + routes::AppState, + types, + types::api::{ + enums, + verify_connector::{self as verify_connector_types, VerifyConnector}, + }, + utils::verify_connector as verify_connector_utils, +}; + +pub async fn generate_access_token(state: AppState) -> RouterResult { + let connector = enums::Connector::Paypal; + let boxed_connector = types::api::ConnectorData::convert_connector( + &state.conf.connectors, + connector.to_string().as_str(), + )?; + let connector_auth = super::get_connector_auth(connector, &state.conf.connector_onboarding)?; + + connector::Paypal::get_access_token( + &state, + verify_connector_types::VerifyConnectorData { + connector: *boxed_connector, + connector_auth, + card_details: verify_connector_utils::get_test_card_details(connector)? + .ok_or(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: connector.to_string(), + }) + .into_report()?, + }, + ) + .await? + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Error occurred while retrieving access token") +} + +pub fn build_paypal_post_request( + url: String, + body: T, + access_token: String, +) -> RouterResult +where + T: serde::Serialize, +{ + let body = types::RequestBody::log_and_get_request_body( + &json!(body), + Encode::::encode_to_string_of_json, + ) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to build request body")?; + + Ok(RequestBuilder::new() + .method(Method::Post) + .url(&url) + .attach_default_headers() + .header( + header::AUTHORIZATION.to_string().as_str(), + format!("Bearer {}", access_token).as_str(), + ) + .header( + header::CONTENT_TYPE.to_string().as_str(), + "application/json", + ) + .body(Some(body)) + .build()) +} + +pub fn build_paypal_get_request(url: String, access_token: String) -> RouterResult { + Ok(RequestBuilder::new() + .method(Method::Get) + .url(&url) + .attach_default_headers() + .header( + header::AUTHORIZATION.to_string().as_str(), + format!("Bearer {}", access_token).as_str(), + ) + .build()) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index d35090551de7..4948bdd575b3 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -301,6 +301,10 @@ pub enum Flow { InviteUser, /// Incremental Authorization flow PaymentsIncrementalAuthorization, + /// Get action URL for connector onboarding + GetActionUrl, + /// Sync connector onboarding status + SyncOnboardingStatus, } /// diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index bec1074b99d0..2159d2d7994f 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -262,3 +262,9 @@ connection_timeout = 10 [kv_config] ttl = 300 # 5 * 60 seconds + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true From 792e642ad58f90bae3ddcea5e6cbc70e948d8e28 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:06:28 +0530 Subject: [PATCH 149/443] feat(pm_list): add required fields for bancontact_card for Mollie, Adyen and Stripe (#3035) --- crates/router/src/configs/defaults.rs | 74 ++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index d529ae034a86..1394c33b5505 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4177,13 +4177,85 @@ impl Default for super::settings::RequiredFields { ConnectorFields { fields: HashMap::from([ ( - enums::Connector::Stripe, + enums::Connector::Mollie, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), common: HashMap::new(), } ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.bancontact_card.billing_details.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.billing_details.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ) + ]), + } + ), + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common:HashMap::from([ + ( + "payment_method_data.bank_redirect.bancontact_card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.bancontact_card.card_holder_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.bancontact_card.card_holder_name".to_string(), + display_name: "card_holder_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ) + ]), + } + ) ]), }, ), From 055d8383671f6b466297c177bcc770618c7da96a Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:21:32 +0530 Subject: [PATCH 150/443] feat: implement FRM flows (#2968) Co-authored-by: Kashif Co-authored-by: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Co-authored-by: Kashif --- config/config.example.toml | 4 + config/development.toml | 4 + config/docker_compose.toml | 5 +- crates/api_models/Cargo.toml | 3 +- crates/api_models/src/enums.rs | 30 + crates/common_enums/src/enums.rs | 1 + crates/common_utils/src/events.rs | 1 + crates/router/Cargo.toml | 3 +- crates/router/src/configs/settings.rs | 9 + crates/router/src/connector.rs | 7 +- crates/router/src/connector/signifyd.rs | 648 +++++++++++++++ .../src/connector/signifyd/transformers.rs | 7 + .../connector/signifyd/transformers/api.rs | 589 ++++++++++++++ .../connector/signifyd/transformers/auth.rs | 20 + crates/router/src/connector/utils.rs | 50 ++ crates/router/src/core.rs | 2 + crates/router/src/core/admin.rs | 8 +- crates/router/src/core/fraud_check.rs | 770 ++++++++++++++++++ crates/router/src/core/fraud_check/flows.rs | 36 + .../core/fraud_check/flows/checkout_flow.rs | 147 ++++ .../fraud_check/flows/fulfillment_flow.rs | 110 +++ .../core/fraud_check/flows/record_return.rs | 149 ++++ .../src/core/fraud_check/flows/sale_flow.rs | 145 ++++ .../fraud_check/flows/transaction_flow.rs | 158 ++++ .../router/src/core/fraud_check/operation.rs | 106 +++ .../fraud_check/operation/fraud_check_post.rs | 457 +++++++++++ .../fraud_check/operation/fraud_check_pre.rs | 337 ++++++++ crates/router/src/core/fraud_check/types.rs | 208 +++++ crates/router/src/core/payments.rs | 329 +++++--- crates/router/src/core/payments/flows.rs | 496 +++++++++++ crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 25 +- crates/router/src/routes/fraud_check.rs | 42 + crates/router/src/routes/lock_utils.rs | 2 +- crates/router/src/routes/payments.rs | 152 ++++ crates/router/src/services/api.rs | 2 + crates/router/src/types.rs | 5 +- crates/router/src/types/api.rs | 22 +- crates/router/src/types/api/fraud_check.rs | 91 +++ crates/router/src/types/fraud_check.rs | 126 +++ crates/router/src/types/storage.rs | 18 +- .../router/src/types/storage/fraud_check.rs | 3 + crates/router/src/types/transformers.rs | 2 +- crates/router_env/src/logger/types.rs | 2 + loadtest/config/development.toml | 5 +- 45 files changed, 5188 insertions(+), 150 deletions(-) create mode 100644 crates/router/src/connector/signifyd.rs create mode 100644 crates/router/src/connector/signifyd/transformers.rs create mode 100644 crates/router/src/connector/signifyd/transformers/api.rs create mode 100644 crates/router/src/connector/signifyd/transformers/auth.rs create mode 100644 crates/router/src/core/fraud_check.rs create mode 100644 crates/router/src/core/fraud_check/flows.rs create mode 100644 crates/router/src/core/fraud_check/flows/checkout_flow.rs create mode 100644 crates/router/src/core/fraud_check/flows/fulfillment_flow.rs create mode 100644 crates/router/src/core/fraud_check/flows/record_return.rs create mode 100644 crates/router/src/core/fraud_check/flows/sale_flow.rs create mode 100644 crates/router/src/core/fraud_check/flows/transaction_flow.rs create mode 100644 crates/router/src/core/fraud_check/operation.rs create mode 100644 crates/router/src/core/fraud_check/operation/fraud_check_post.rs create mode 100644 crates/router/src/core/fraud_check/operation/fraud_check_pre.rs create mode 100644 crates/router/src/core/fraud_check/types.rs create mode 100644 crates/router/src/routes/fraud_check.rs create mode 100644 crates/router/src/types/api/fraud_check.rs create mode 100644 crates/router/src/types/fraud_check.rs create mode 100644 crates/router/src/types/storage/fraud_check.rs diff --git a/config/config.example.toml b/config/config.example.toml index fad4da3e7c36..1897c9355812 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -215,6 +215,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -478,6 +479,9 @@ connection_timeout = 10 # Timeout for database connection in seconds # TTL for KV in seconds ttl = 900 +[frm] +enabled = true + [paypal_onboarding] client_id = "paypal_client_id" # Client ID for PayPal onboarding client_secret = "paypal_secret_key" # Secret key for PayPal onboarding diff --git a/config/development.toml b/config/development.toml index 2eb8b00b9c08..4ee33795676c 100644 --- a/config/development.toml +++ b/config/development.toml @@ -189,6 +189,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -476,6 +477,9 @@ delay_between_retries_in_milliseconds = 500 [kv_config] ttl = 900 # 15 * 60 seconds +[frm] +enabled = true + [events] source = "logs" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index de90f3c70abd..55fc62329d4c 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -129,6 +129,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -363,8 +364,10 @@ queue_strategy = "Fifo" [kv_config] ttl = 900 # 15 * 60 seconds +[frm] +enabled = true + [connector_onboarding.paypal] client_id = "" client_secret = "" partner_id = "" -enabled = true diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index cb2e243745de..116aad25d5c8 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -default = ["payouts"] +default = ["payouts", "frm"] business_profile_routing = [] connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] @@ -17,6 +17,7 @@ connector_choice_mca_id = ["euclid/connector_choice_mca_id"] dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] +frm = [] [dependencies] actix-web = { version = "4.3.1", optional = true } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 535be4dfb159..17787929a463 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -178,6 +178,36 @@ impl From for RoutableConnectors { } } +#[cfg(feature = "frm")] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum FrmConnectors { + /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/ + Signifyd, +} + +#[cfg(feature = "frm")] +impl From for RoutableConnectors { + fn from(value: FrmConnectors) -> Self { + match value { + FrmConnectors::Signifyd => Self::Signifyd, + } + } +} + #[derive( Clone, Copy, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 7615c0cc8804..980f98db1519 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -146,6 +146,7 @@ pub enum RoutableConnectors { Prophetpay, Rapyd, Shift4, + Signifyd, Square, Stax, Stripe, diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 14b8d4de1c36..c9efbb73c208 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -45,6 +45,7 @@ pub enum ApiEventsType { // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, RustLocker, + FraudCheck, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index f508460574dd..791f617b30df 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,10 +9,11 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry", "frm"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] +frm = [] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 68af91d06612..6cbffc186d23 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -113,6 +113,8 @@ pub struct Settings { pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, + #[cfg(feature = "frm")] + pub frm: Frm, #[cfg(feature = "olap")] pub report_download_config: ReportConfig, pub events: EventsConfig, @@ -120,6 +122,12 @@ pub struct Settings { pub connector_onboarding: ConnectorOnboarding, } +#[cfg(feature = "frm")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Frm { + pub enabled: bool, +} + #[derive(Debug, Deserialize, Clone)] pub struct KvConfig { pub ttl: u32, @@ -603,6 +611,7 @@ pub struct Connectors { pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, pub shift4: ConnectorParams, + pub signifyd: ConnectorParams, pub square: ConnectorParams, pub stax: ConnectorParams, pub stripe: ConnectorParamsWithFileUploadUrl, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 3a83fea0d910..55c61442591d 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -40,6 +40,7 @@ pub mod powertranz; pub mod prophetpay; pub mod rapyd; pub mod shift4; +pub mod signifyd; pub mod square; pub mod stax; pub mod stripe; @@ -63,7 +64,7 @@ pub use self::{ iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, powertranz::Powertranz, - prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, - stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, - worldpay::Worldpay, zen::Zen, + prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, signifyd::Signifyd, square::Square, + stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, + worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs new file mode 100644 index 000000000000..5d9714e4d945 --- /dev/null +++ b/crates/router/src/connector/signifyd.rs @@ -0,0 +1,648 @@ +pub mod transformers; +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::PeekInterface; +use transformers as signifyd; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + }, +}; +#[cfg(feature = "frm")] +use crate::{ + services, + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Signifyd; + +impl ConnectorCommonExt for Signifyd +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Signifyd { + fn id(&self) -> &'static str { + "signifyd" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.signifyd.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = signifyd::SignifydAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth_api_key = format!("Basic {}", auth.api_key.peek()); + + Ok(vec![( + headers::AUTHORIZATION.to_string(), + request::Mask::into_masked(auth_api_key), + )]) + } + + #[cfg(feature = "frm")] + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydErrorResponse = res + .response + .parse_struct("SignifydErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.messages.join(" &"), + reason: Some(response.errors.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl api::Payment for Signifyd {} +impl api::PaymentAuthorize for Signifyd {} +impl api::PaymentSync for Signifyd {} +impl api::PaymentVoid for Signifyd {} +impl api::PaymentCapture for Signifyd {} +impl api::MandateSetup for Signifyd {} +impl api::ConnectorAccessToken for Signifyd {} +impl api::PaymentToken for Signifyd {} +impl api::Refund for Signifyd {} +impl api::RefundExecute for Signifyd {} +impl api::RefundSync for Signifyd {} +impl ConnectorValidation for Signifyd {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Signifyd +{ +} + +impl api::PaymentSession for Signifyd {} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration for Signifyd {} + +#[cfg(feature = "frm")] +impl api::FraudCheck for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckSale for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckCheckout for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckTransaction for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckFulfillment for Signifyd {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckRecordReturn for Signifyd {} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmSaleRouterData, + 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: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/sales" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmSaleRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsSaleRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmSaleType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmSaleType::get_headers(self, req, connectors)?) + .body(frm_types::FrmSaleType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmSaleRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Sale") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmCheckoutRouterData, + 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: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/checkouts" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmCheckoutRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsCheckoutRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmCheckoutType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmCheckoutType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmCheckoutType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmCheckoutRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Checkout") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmTransactionRouterData, + 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: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/transactions" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmTransactionRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsTransactionRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmTransactionType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmTransactionType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmTransactionType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmTransactionRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmFulfillmentRouterData, + 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: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/fulfillments" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmFulfillmentRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = &req.request.fulfillment_request; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmFulfillmentType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmFulfillmentType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmFulfillmentType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmFulfillmentRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::FrmFullfillmentSignifydApiResponse = res + .response + .parse_struct("FrmFullfillmentSignifydApiResponse Sale") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + frm_types::FrmFulfillmentRouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmRecordReturnRouterData, + 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: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/returns/records" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmRecordReturnRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsRecordReturnRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmRecordReturnType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmRecordReturnType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmRecordReturnType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmRecordReturnRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsRecordReturnResponse = res + .response + .parse_struct("SignifydPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Signifyd { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/signifyd/transformers.rs b/crates/router/src/connector/signifyd/transformers.rs new file mode 100644 index 000000000000..4f155f341f6d --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "frm")] +pub mod api; +pub mod auth; + +#[cfg(feature = "frm")] +pub use self::api::*; +pub use self::auth::*; diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs new file mode 100644 index 000000000000..1a1b09bd2880 --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -0,0 +1,589 @@ +use bigdecimal::ToPrimitive; +use common_utils::pii::Email; +use error_stack; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; +use utoipa::ToSchema; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, + FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, + }, + core::{ + errors, + fraud_check::types::{self as core_types, FrmFulfillmentRequest}, + }, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +#[allow(dead_code)] +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DecisionDelivery { + Sync, + AsyncOnly, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Purchase { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + order_channel: OrderChannel, + total_price: i64, + products: Vec, + shipments: Shipments, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderChannel { + Web, + Phone, + MobileApp, + Social, + Marketplace, + InStoreKiosk, + ScanAndGo, + SmartTv, + Mit, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Products { + item_name: String, + item_price: i64, + item_quantity: i32, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +pub struct Shipments { + destination: Destination, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Destination { + full_name: Secret, + organization: Option, + email: Option, + address: Address, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Address { + street_address: Secret, + unit: Option>, + postal_code: Secret, + city: String, + province_code: Secret, + country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsSaleRequest { + order_id: String, + purchase: Purchase, + decision_delivery: DecisionDelivery, +} + +impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmSaleRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + purchase, + decision_delivery: DecisionDelivery::Sync, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Decision { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + checkpoint_action: SignifydPaymentStatus, + checkpoint_action_reason: Option, + checkpoint_action_policy: Option, + score: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SignifydPaymentStatus { + Accept, + Challenge, + Credit, + Hold, + Reject, +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: SignifydPaymentStatus) -> Self { + match item { + SignifydPaymentStatus::Accept => Self::Legit, + SignifydPaymentStatus::Reject => Self::Fraud, + SignifydPaymentStatus::Hold => Self::ManualReview, + SignifydPaymentStatus::Challenge | SignifydPaymentStatus::Credit => Self::Pending, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsResponse { + signifyd_id: i64, + order_id: String, + decision: Decision, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + status: storage_enums::FraudCheckStatus::from( + item.response.decision.checkpoint_action, + ), + connector_metadata: None, + score: item.response.decision.score.and_then(|data| data.to_i32()), + reason: item + .response + .decision + .checkpoint_action_reason + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct SignifydErrorResponse { + pub messages: Vec, + pub errors: serde_json::Value, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Transactions { + transaction_id: String, + gateway_status_code: String, + payment_method: storage_enums::PaymentMethod, + amount: i64, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsTransactionRequest { + order_id: String, + checkout_id: String, + transactions: Transactions, +} + +impl From for GatewayStatusCode { + fn from(item: storage_enums::AttemptStatus) -> Self { + match item { + storage_enums::AttemptStatus::Pending => Self::Pending, + storage_enums::AttemptStatus::Failure => Self::Failure, + storage_enums::AttemptStatus::Charged => Self::Success, + _ => Self::Pending, + } + } +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for SignifydPaymentsTransactionRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + let currency = item.request.get_currency()?; + let transactions = Transactions { + amount: item.request.amount, + transaction_id: item.clone().payment_id, + gateway_status_code: GatewayStatusCode::from(item.status).to_string(), + payment_method: item.payment_method, + currency, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + checkout_id: item.payment_id.clone(), + transactions, + }) + } +} + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum GatewayStatusCode { + Success, + Failure, + #[default] + Pending, + Error, + Cancelled, + Expired, + SoftDecline, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsCheckoutRequest { + checkout_id: String, + order_id: String, + purchase: Purchase, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmCheckoutRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments: Shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + checkout_id: item.payment_id.clone(), + order_id: item.attempt_id.clone(), + purchase, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiRequest { + pub order_id: String, + pub fulfillment_status: Option, + pub fulfillments: Vec, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Fulfillments { + pub shipment_id: String, + pub products: Option>, + pub destination: Destination, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +impl From for FrmFullfillmentSignifydApiRequest { + fn from(req: FrmFulfillmentRequest) -> Self { + Self { + order_id: req.order_id, + fulfillment_status: req.fulfillment_status.map(FulfillmentStatus::from), + fulfillments: req + .fulfillments + .iter() + .map(|f| Fulfillments::from(f.clone())) + .collect(), + } + } +} + +impl From for FulfillmentStatus { + fn from(status: core_types::FulfillmentStatus) -> Self { + match status { + core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, + core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, + core_types::FulfillmentStatus::REPLACEMENT => Self::REPLACEMENT, + core_types::FulfillmentStatus::CANCELED => Self::CANCELED, + } + } +} + +impl From for Fulfillments { + fn from(fulfillment: core_types::Fulfillments) -> Self { + Self { + shipment_id: fulfillment.shipment_id, + products: fulfillment + .products + .map(|products| products.iter().map(|p| Product::from(p.clone())).collect()), + destination: Destination::from(fulfillment.destination), + } + } +} + +impl From for Product { + fn from(product: core_types::Product) -> Self { + Self { + item_name: product.item_name, + item_quantity: product.item_quantity, + item_id: product.item_id, + } + } +} + +impl From for Destination { + fn from(destination: core_types::Destination) -> Self { + Self { + full_name: destination.full_name, + organization: destination.organization, + email: destination.email, + address: Address::from(destination.address), + } + } +} + +impl From for Address { + fn from(address: core_types::Address) -> Self { + Self { + street_address: address.street_address, + unit: address.unit, + postal_code: address.postal_code, + city: address.city, + province_code: address.province_code, + country_code: address.country_code, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiResponse { + pub order_id: String, + pub shipment_ids: Vec, +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + > + for types::RouterData< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order_id, + shipment_ids: item.response.shipment_ids, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydRefund { + method: RefundMethod, + amount: String, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnRequest { + order_id: String, + return_id: String, + refund_transaction_id: Option, + refund: SignifydRefund, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RefundMethod { + StoreCredit, + OriginalPaymentInstrument, + NewPaymentInstrument, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnResponse { + return_id: String, + order_id: String, +} + +impl + TryFrom< + ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + return_id: Some(item.response.return_id.to_string()), + connector_metadata: None, + }), + ..item.data + }) + } +} + +impl TryFrom<&frm_types::FrmRecordReturnRouterData> for SignifydPaymentsRecordReturnRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmRecordReturnRouterData) -> Result { + let currency = item.request.get_currency()?; + let refund = SignifydRefund { + method: item.request.refund_method.clone(), + amount: item.request.amount.to_string(), + currency, + }; + Ok(Self { + return_id: uuid::Uuid::new_v4().to_string(), + refund_transaction_id: item.request.refund_transaction_id.clone(), + refund, + order_id: item.attempt_id.clone(), + }) + } +} diff --git a/crates/router/src/connector/signifyd/transformers/auth.rs b/crates/router/src/connector/signifyd/transformers/auth.rs new file mode 100644 index 000000000000..cc5867aea366 --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers/auth.rs @@ -0,0 +1,20 @@ +use error_stack; +use masking::Secret; + +use crate::{core::errors, types}; + +pub struct SignifydAuthType { + pub api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for SignifydAuthType { + 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_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 803c511f3a6b..2580dcd3fc22 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -17,6 +17,8 @@ use once_cell::sync::Lazy; use regex::Regex; use serde::Serializer; +#[cfg(feature = "frm")] +use crate::types::{fraud_check, storage::enums as storage_enums}; use crate::{ consts, core::{ @@ -1575,3 +1577,51 @@ pub fn validate_currency( } Ok(()) } + +#[cfg(feature = "frm")] +pub trait FraudCheckSaleRequest { + fn get_order_details(&self) -> Result, Error>; +} +#[cfg(feature = "frm")] +impl FraudCheckSaleRequest for fraud_check::FraudCheckSaleData { + fn get_order_details(&self) -> Result, Error> { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } +} + +#[cfg(feature = "frm")] +pub trait FraudCheckCheckoutRequest { + fn get_order_details(&self) -> Result, Error>; +} +#[cfg(feature = "frm")] +impl FraudCheckCheckoutRequest for fraud_check::FraudCheckCheckoutData { + fn get_order_details(&self) -> Result, Error> { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } +} + +#[cfg(feature = "frm")] +pub trait FraudCheckTransactionRequest { + fn get_currency(&self) -> Result; +} +#[cfg(feature = "frm")] +impl FraudCheckTransactionRequest for fraud_check::FraudCheckTransactionData { + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} + +#[cfg(feature = "frm")] +pub trait FraudCheckRecordReturnRequest { + fn get_currency(&self) -> Result; +} +#[cfg(feature = "frm")] +impl FraudCheckRecordReturnRequest for fraud_check::FraudCheckRecordReturnData { + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 6a167be48dae..be83de849161 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -13,6 +13,8 @@ pub mod customers; pub mod disputes; pub mod errors; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod gsm; pub mod locker_migration; pub mod mandate; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 107e8f8859d6..5ab543d382f5 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1716,10 +1716,12 @@ pub(crate) fn validate_auth_and_metadata_type( zen::transformers::ZenAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Signifyd | api_enums::Connector::Plaid => { - Err(report!(errors::ConnectorError::InvalidConnectorName) - .attach_printable(format!("invalid connector name: {connector_name}"))) + api_enums::Connector::Signifyd => { + signifyd::transformers::SignifydAuthType::try_from(val)?; + Ok(()) } + api_enums::Connector::Plaid => Err(report!(errors::ConnectorError::InvalidConnectorName) + .attach_printable(format!("invalid connector name: {connector_name}"))), } } diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs new file mode 100644 index 000000000000..55bd22baeec4 --- /dev/null +++ b/crates/router/src/core/fraud_check.rs @@ -0,0 +1,770 @@ +use std::fmt::Debug; + +use api_models::{admin::FrmConfigs, enums as api_enums, payments::AdditionalPaymentData}; +use error_stack::ResultExt; +use masking::PeekInterface; +use router_env::{ + logger, + tracing::{self, instrument}, +}; + +use self::{ + flows::{self as frm_flows, FeatureFrm}, + types::{ + self as frm_core_types, ConnectorDetailsCore, FrmConfigsObject, FrmData, FrmInfo, + PaymentDetails, PaymentToFrmData, + }, +}; +use super::errors::{ConnectorErrorExt, RouterResponse}; +use crate::{ + connector::signifyd::transformers::FrmFullfillmentSignifydApiRequest, + core::{ + errors::{self, RouterResult}, + payments::{ + self, flows::ConstructFlowSpecificData, helpers::get_additional_payment_data, + operations::BoxedOperation, + }, + utils as core_utils, + }, + db::StorageInterface, + routes::AppState, + services, + types::{ + self as oss_types, + api::{routing::FrmRoutingAlgorithm, Connector, FraudCheckConnectorData, Fulfillment}, + domain, fraud_check as frm_types, + storage::{ + enums::{ + AttemptStatus, FraudCheckLastStep, FraudCheckStatus, FraudCheckType, FrmSuggestion, + IntentStatus, + }, + fraud_check::{FraudCheck, FraudCheckUpdate}, + PaymentIntent, + }, + }, + utils::ValueExt, +}; +pub mod flows; +pub mod operation; +pub mod types; + +#[instrument(skip_all)] +pub async fn call_frm_service( + state: &AppState, + payment_data: &mut payments::PaymentData, + frm_data: FrmData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, +) -> RouterResult> +where + F: Send + Clone, + + // To create connector flow specific interface data + FrmData: ConstructFlowSpecificData, + oss_types::RouterData: FeatureFrm + Send, + + // To construct connector flow specific api + dyn Connector: services::api::ConnectorIntegration, +{ + let merchant_connector_account = payments::construct_profile_id_and_get_mca( + state, + merchant_account, + payment_data, + &frm_data.connector_details.connector_name, + None, + key_store, + false, + ) + .await?; + + let router_data = frm_data + .construct_router_data( + state, + &frm_data.connector_details.connector_name, + merchant_account, + key_store, + customer, + &merchant_connector_account, + ) + .await?; + let connector = + FraudCheckConnectorData::get_connector_by_name(&frm_data.connector_details.connector_name)?; + let router_data_res = router_data + .decide_frm_flows( + state, + &connector, + payments::CallConnectorAction::Trigger, + merchant_account, + ) + .await?; + + Ok(router_data_res) +} + +pub async fn should_call_frm( + merchant_account: &domain::MerchantAccount, + payment_data: &payments::PaymentData, + db: &dyn StorageInterface, + key_store: domain::MerchantKeyStore, +) -> RouterResult<( + bool, + Option, + Option, + Option, +)> +where + F: Send + Clone, +{ + match merchant_account.frm_routing_algorithm.clone() { + Some(frm_routing_algorithm_value) => { + let frm_routing_algorithm_struct: FrmRoutingAlgorithm = frm_routing_algorithm_value + .clone() + .parse_value("FrmRoutingAlgorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_routing_algorithm", + }) + .attach_printable("Data field not found in frm_routing_algorithm")?; + + let profile_id = core_utils::get_profile_id_from_business_details( + payment_data.payment_intent.business_country, + payment_data.payment_intent.business_label.as_ref(), + merchant_account, + payment_data.payment_intent.profile_id.as_ref(), + db, + false, + ) + .await + .attach_printable("Could not find profile id from business details")?; + + let merchant_connector_account_from_db_option = db + .find_merchant_connector_account_by_profile_id_connector_name( + &profile_id, + &frm_routing_algorithm_struct.data, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + }) + .ok(); + + match merchant_connector_account_from_db_option { + Some(merchant_connector_account_from_db) => { + let frm_configs_option = merchant_connector_account_from_db + .frm_configs + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_configs", + }) + .ok(); + match frm_configs_option { + Some(frm_configs_value) => { + let frm_configs_struct: Vec = frm_configs_value + .iter() + .map(|config| { config + .peek() + .clone() + .parse_value("FrmConfigs") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "frm_configs".to_string(), + expected_format: r#"[{ "gateway": "stripe", "payment_methods": [{ "payment_method": "card","payment_method_types": [{"payment_method_type": "credit","card_networks": ["Visa"],"flow": "pre","action": "cancel_txn"}]}]}]"#.to_string(), + }) + }) + .collect::, _>>()?; + + let mut is_frm_connector_enabled = false; + let mut is_frm_pm_enabled = false; + let mut is_frm_pmt_enabled = false; + let filtered_frm_config = frm_configs_struct + .iter() + .filter(|frm_config| { + match ( + &payment_data.clone().payment_attempt.connector, + &frm_config.gateway, + ) { + (Some(current_connector), Some(configured_connector)) => { + let is_enabled = *current_connector + == configured_connector.to_string(); + if is_enabled { + is_frm_connector_enabled = true; + } + is_enabled + } + (None, _) | (_, None) => true, + } + }) + .collect::>(); + let filtered_payment_methods = filtered_frm_config + .iter() + .map(|frm_config| { + let filtered_frm_config_by_pm = frm_config + .payment_methods + .iter() + .filter(|frm_config_pm| { + match ( + payment_data.payment_attempt.payment_method, + frm_config_pm.payment_method, + ) { + ( + Some(current_pm), + Some(configured_connector_pm), + ) => { + let is_enabled = current_pm.to_string() + == configured_connector_pm.to_string(); + if is_enabled { + is_frm_pm_enabled = true; + } + is_enabled + } + (None, _) | (_, None) => true, + } + }) + .collect::>(); + filtered_frm_config_by_pm + }) + .collect::>() + .concat(); + let additional_payment_data = match &payment_data.payment_method_data { + Some(pmd) => { + let additional_payment_data = + get_additional_payment_data(pmd, db).await; + Some(additional_payment_data) + } + None => payment_data + .payment_attempt + .payment_method_data + .as_ref() + .map(|pm_data| { + pm_data.clone().parse_value::( + "AdditionalPaymentData", + ) + }) + .transpose() + .unwrap_or_default(), // Making this default in case of error as we don't want to fail payment for frm errors + }; + let filtered_payment_method_types = filtered_payment_methods + .iter() + .map(|frm_pm_config| { + let filtered_pm_config_by_pmt = frm_pm_config + .payment_method_types + .iter() + .filter(|frm_pm_config_by_pmt| { + match ( + &payment_data + .clone() + .payment_attempt + .payment_method_type, + frm_pm_config_by_pmt.payment_method_type, + ) { + (Some(curr), Some(conf)) + if curr.to_string() == conf.to_string() => + { + is_frm_pmt_enabled = true; + true + } + (None, Some(conf)) => match additional_payment_data + .clone() + { + Some(AdditionalPaymentData::Card(card)) => { + let card_type = card + .card_type + .unwrap_or_else(|| "debit".to_string()); + let is_enabled = card_type.to_lowercase() + == conf.to_string().to_lowercase(); + if is_enabled { + is_frm_pmt_enabled = true; + } + is_enabled + } + _ => false, + }, + _ => false, + } + }) + .collect::>(); + filtered_pm_config_by_pmt + }) + .collect::>() + .concat(); + let is_frm_enabled = + is_frm_connector_enabled && is_frm_pm_enabled && is_frm_pmt_enabled; + logger::debug!( + "frm_configs {:?} {:?} {:?} {:?}", + is_frm_connector_enabled, + is_frm_pm_enabled, + is_frm_pmt_enabled, + is_frm_enabled + ); + // filtered_frm_config... + // Panic Safety: we are first checking if the object is present... only if present, we try to fetch index 0 + let frm_configs_object = FrmConfigsObject { + frm_enabled_gateway: filtered_frm_config + .get(0) + .and_then(|c| c.gateway), + frm_enabled_pm: filtered_payment_methods + .get(0) + .and_then(|pm| pm.payment_method), + frm_enabled_pm_type: filtered_payment_method_types + .get(0) + .and_then(|pmt| pmt.payment_method_type), + frm_action: filtered_payment_method_types + // .clone() + .get(0) + .map(|pmt| pmt.action.clone()) + .unwrap_or(api_enums::FrmAction::ManualReview), + frm_preferred_flow_type: filtered_payment_method_types + .get(0) + .map(|pmt| pmt.flow.clone()) + .unwrap_or(api_enums::FrmPreferredFlowTypes::Pre), + }; + logger::debug!( + "frm_routing_configs: {:?} {:?} {:?} {:?}", + frm_routing_algorithm_struct, + profile_id, + frm_configs_object, + is_frm_enabled + ); + Ok(( + is_frm_enabled, + Some(frm_routing_algorithm_struct), + Some(profile_id.to_string()), + Some(frm_configs_object), + )) + } + None => { + logger::error!("Cannot find frm_configs for FRM provider"); + Ok((false, None, None, None)) + } + } + } + None => { + logger::error!("Cannot find merchant connector account for FRM provider"); + Ok((false, None, None, None)) + } + } + } + _ => Ok((false, None, None, None)), + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn make_frm_data_and_fraud_check_operation<'a, F>( + _db: &dyn StorageInterface, + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: payments::PaymentData, + frm_routing_algorithm: FrmRoutingAlgorithm, + profile_id: String, + frm_configs: FrmConfigsObject, + _customer: &Option, +) -> RouterResult> +where + F: Send + Clone, +{ + let order_details = payment_data + .payment_intent + .order_details + .clone() + .or_else(|| + // when the order_details are present within the meta_data, we need to take those to support backward compatibility + payment_data.payment_intent.metadata.clone().and_then(|meta| { + let order_details = meta.peek().get("order_details").to_owned(); + order_details.map(|order| vec![masking::Secret::new(order.to_owned())]) + })) + .map(|order_details_value| { + order_details_value + .into_iter() + .map(|data| { + data.peek() + .to_owned() + .parse_value("OrderDetailsWithAmount") + .attach_printable("unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + .unwrap_or_default() + }); + + let frm_connector_details = ConnectorDetailsCore { + connector_name: frm_routing_algorithm.data, + profile_id, + }; + + let payment_to_frm_data = PaymentToFrmData { + amount: payment_data.amount, + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: merchant_account.to_owned(), + address: payment_data.address.clone(), + connector_details: frm_connector_details.clone(), + order_details, + }; + + let fraud_check_operation: operation::BoxedFraudCheckOperation = + match frm_configs.frm_preferred_flow_type { + api_enums::FrmPreferredFlowTypes::Pre => Box::new(operation::FraudCheckPre), + api_enums::FrmPreferredFlowTypes::Post => Box::new(operation::FraudCheckPost), + }; + let frm_data = fraud_check_operation + .to_get_tracker()? + .get_trackers(state, payment_to_frm_data, frm_connector_details) + .await?; + Ok(FrmInfo { + fraud_check_operation, + frm_data, + suggested_action: None, + }) +} + +#[allow(clippy::too_many_arguments)] +pub async fn pre_payment_frm_core<'a, F>( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + frm_info: &mut FrmInfo, + frm_configs: FrmConfigsObject, + customer: &Option, + should_continue_transaction: &mut bool, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if let Some(frm_data) = &mut frm_info.frm_data { + if matches!( + frm_configs.frm_preferred_flow_type, + api_enums::FrmPreferredFlowTypes::Pre + ) { + let fraud_check_operation = &mut frm_info.fraud_check_operation; + + let frm_router_data = fraud_check_operation + .to_domain()? + .pre_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store, + ) + .await?; + let frm_data_updated = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.clone(), + payment_data, + None, + frm_router_data, + ) + .await?; + let frm_fraud_check = frm_data_updated.fraud_check.clone(); + payment_data.frm_message = Some(frm_fraud_check.clone()); + if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) + //DontTakeAction + { + *should_continue_transaction = false; + if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) { + frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction); + } else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) { + frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview); + } + } + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_info.fraud_check_operation, + frm_info.suggested_action + ); + Ok(Some(frm_data_updated)) + } else { + Ok(Some(frm_data.to_owned())) + } + } else { + Ok(None) + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn post_payment_frm_core<'a, F>( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + frm_info: &mut FrmInfo, + frm_configs: FrmConfigsObject, + customer: &Option, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if let Some(frm_data) = &mut frm_info.frm_data { + // Allow the Post flow only if the payment is succeeded, + // this logic has to be removed if we are going to call /sale or /transaction after failed transaction + let fraud_check_operation = &mut frm_info.fraud_check_operation; + if payment_data.payment_attempt.status == AttemptStatus::Charged { + let frm_router_data_opt = fraud_check_operation + .to_domain()? + .post_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store.clone(), + ) + .await?; + if let Some(frm_router_data) = frm_router_data_opt { + let mut frm_data = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.to_owned(), + payment_data, + None, + frm_router_data.to_owned(), + ) + .await?; + + payment_data.frm_message = Some(frm_data.fraud_check.clone()); + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_data, + payment_data.frm_message + ); + let mut frm_suggestion = None; + fraud_check_operation + .to_domain()? + .execute_post_tasks( + state, + &mut frm_data, + merchant_account, + frm_configs, + &mut frm_suggestion, + key_store, + payment_data, + customer, + ) + .await?; + logger::debug!("frm_post_tasks_data: {:?}", frm_data); + let updated_frm_data = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.to_owned(), + payment_data, + frm_suggestion, + frm_router_data.to_owned(), + ) + .await?; + return Ok(Some(updated_frm_data)); + } + } + + Ok(Some(frm_data.to_owned())) + } else { + Ok(None) + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn call_frm_before_connector_call<'a, F, Req, Ctx>( + db: &dyn StorageInterface, + operation: &BoxedOperation<'_, F, Req, Ctx>, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + state: &AppState, + frm_info: &mut Option>, + customer: &Option, + should_continue_transaction: &mut bool, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if is_operation_allowed(operation) { + let (is_frm_enabled, frm_routing_algorithm, frm_connector_label, frm_configs) = + should_call_frm(merchant_account, payment_data, db, key_store.clone()).await?; + if let Some((frm_routing_algorithm_val, profile_id)) = + frm_routing_algorithm.zip(frm_connector_label) + { + if let Some(frm_configs) = frm_configs.clone() { + let mut updated_frm_info = make_frm_data_and_fraud_check_operation( + db, + state, + merchant_account, + payment_data.to_owned(), + frm_routing_algorithm_val, + profile_id, + frm_configs.clone(), + customer, + ) + .await?; + + if is_frm_enabled { + pre_payment_frm_core( + state, + merchant_account, + payment_data, + &mut updated_frm_info, + frm_configs, + customer, + should_continue_transaction, + key_store, + ) + .await?; + } + *frm_info = Some(updated_frm_info); + } + } + logger::debug!("frm_configs: {:?} {:?}", frm_configs, is_frm_enabled); + return Ok(frm_configs); + } + Ok(None) +} + +pub fn is_operation_allowed(operation: &Op) -> bool { + !["PaymentSession", "PaymentApprove", "PaymentReject"] + .contains(&format!("{operation:?}").as_str()) +} + +impl From for PaymentDetails { + fn from(payment_data: PaymentToFrmData) -> Self { + Self { + amount: payment_data.amount.into(), + currency: payment_data.payment_attempt.currency, + payment_method: payment_data.payment_attempt.payment_method, + payment_method_type: payment_data.payment_attempt.payment_method_type, + refund_transaction_id: None, + } + } +} + +#[instrument(skip_all)] +pub async fn frm_fulfillment_core( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: frm_core_types::FrmFulfillmentRequest, +) -> RouterResponse { + let db = &*state.clone().store; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &req.payment_id.clone(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + match payment_intent.status { + IntentStatus::Succeeded => { + let invalid_request_error = errors::ApiErrorResponse::InvalidRequestData { + message: "no fraud check entry found for this payment_id".to_string(), + }; + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + req.payment_id.clone(), + merchant_account.merchant_id.clone(), + ) + .await + .change_context(invalid_request_error.to_owned())?; + match existing_fraud_check { + Some(fraud_check) => { + if (matches!(fraud_check.frm_transaction_type, FraudCheckType::PreFrm) + && fraud_check.last_step == FraudCheckLastStep::TransactionOrRecordRefund) + || (matches!(fraud_check.frm_transaction_type, FraudCheckType::PostFrm) + && fraud_check.last_step == FraudCheckLastStep::CheckoutOrSale) + { + Box::pin(make_fulfillment_api_call( + db, + fraud_check, + payment_intent, + state, + merchant_account, + key_store, + req, + )) + .await + } else { + Err(errors::ApiErrorResponse::PreconditionFailed {message:"Frm pre/post flow hasn't terminated yet, so fulfillment cannot be called".to_string(),}.into()) + } + } + None => Err(invalid_request_error.into()), + } + } + _ => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Fulfillment can be performed only for succeeded payment".to_string(), + } + .into()), + } +} + +#[instrument(skip_all)] +pub async fn make_fulfillment_api_call( + db: &dyn StorageInterface, + fraud_check: FraudCheck, + payment_intent: PaymentIntent, + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: frm_core_types::FrmFulfillmentRequest, +) -> RouterResponse { + let payment_attempt = db + .find_payment_attempt_by_attempt_id_merchant_id( + &payment_intent.active_attempt.get_id(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let connector_data = FraudCheckConnectorData::get_connector_by_name(&fraud_check.frm_name)?; + let connector_integration: services::BoxedConnectorIntegration< + '_, + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > = connector_data.connector.get_connector_integration(); + let modified_request_for_api_call = FrmFullfillmentSignifydApiRequest::from(req); + let router_data = frm_flows::fulfillment_flow::construct_fulfillment_router_data( + &state, + &payment_intent, + &payment_attempt, + &merchant_account, + &key_store, + "signifyd".to_string(), + modified_request_for_api_call, + ) + .await?; + let response = services::execute_connector_processing_step( + &state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + let fraud_check_copy = fraud_check.clone(); + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: fraud_check.frm_status, + frm_transaction_id: fraud_check.frm_transaction_id, + frm_reason: fraud_check.frm_reason, + frm_score: fraud_check.frm_score, + metadata: fraud_check.metadata, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Fulfillment, + }; + let _updated = db + .update_fraud_check_response_with_attempt_id(fraud_check_copy, fraud_check_update) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?; + let fulfillment_response = + response + .response + .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector_data.connector_name.clone().to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + Ok(services::ApplicationResponse::Json(fulfillment_response)) +} diff --git a/crates/router/src/core/fraud_check/flows.rs b/crates/router/src/core/fraud_check/flows.rs new file mode 100644 index 000000000000..3d4916a372be --- /dev/null +++ b/crates/router/src/core/fraud_check/flows.rs @@ -0,0 +1,36 @@ +pub mod checkout_flow; +pub mod fulfillment_flow; +pub mod record_return; +pub mod sale_flow; +pub mod transaction_flow; + +use async_trait::async_trait; + +use crate::{ + core::{ + errors::RouterResult, + payments::{self, flows::ConstructFlowSpecificData}, + }, + routes::AppState, + services, + types::{ + api::{Connector, FraudCheckConnectorData}, + domain, + fraud_check::FraudCheckResponseData, + }, +}; + +#[async_trait] +pub trait FeatureFrm { + async fn decide_frm_flows<'a>( + self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult + where + Self: Sized, + F: Clone, + dyn Connector: services::ConnectorIntegration; +} diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs new file mode 100644 index 000000000000..47a29d657484 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -0,0 +1,147 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use super::{ConstructFlowSpecificData, FeatureFrm}; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::types::FrmData, + payments::{self, helpers}, + }, + errors, services, + types::{ + api::fraud_check::{self as frm_api, FraudCheckConnectorData}, + domain, + fraud_check::{FraudCheckCheckoutData, FraudCheckResponseData, FrmCheckoutRouterData}, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> + { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckCheckoutData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + }, // self.order_details + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmCheckoutRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmCheckoutRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Checkout, + FraudCheckCheckoutData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs new file mode 100644 index 000000000000..6865a9510819 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs @@ -0,0 +1,110 @@ +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; +use router_env::tracing::{self, instrument}; + +use crate::{ + connector::signifyd::transformers::FrmFullfillmentSignifydApiRequest, + core::{ + errors::RouterResult, + payments::{helpers, PaymentAddress}, + utils as core_utils, + }, + errors, + types::{ + domain, + fraud_check::{FraudCheckFulfillmentData, FrmFulfillmentRouterData}, + storage, ConnectorAuthType, ErrorResponse, RouterData, + }, + utils, AppState, +}; + +#[instrument(skip_all)] +pub async fn construct_fulfillment_router_data<'a>( + state: &'a AppState, + payment_intent: &'a storage::PaymentIntent, + payment_attempt: &storage::PaymentAttempt, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connector: String, + fulfillment_request: FrmFullfillmentSignifydApiRequest, +) -> RouterResult { + let profile_id = core_utils::get_profile_id_from_business_details( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + merchant_account, + payment_intent.profile_id.as_ref(), + &*state.store, + false, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")?; + + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + merchant_account.merchant_id.as_str(), + None, + key_store, + &profile_id, + &connector, + None, + ) + .await?; + + let test_mode: Option = merchant_connector_account.is_test_mode_on(); + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let payment_method = utils::OptionExt::get_required_value( + payment_attempt.payment_method, + "payment_method_type", + )?; + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + connector, + payment_id: payment_attempt.payment_id.clone(), + attempt_id: payment_attempt.attempt_id.clone(), + status: payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: None, + return_url: payment_intent.return_url.clone(), + payment_method_id: payment_attempt.payment_method_id.clone(), + address: PaymentAddress::default(), + auth_type: payment_attempt.authentication_type.unwrap_or_default(), + connector_meta_data: merchant_connector_account.get_metadata(), + amount_captured: payment_intent.amount_captured, + request: FraudCheckFulfillmentData { + amount: payment_attempt.amount, + order_details: payment_intent.order_details.clone(), + fulfillment_request, + }, + response: Err(ErrorResponse::default()), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + customer_id: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + payment_method_balance: None, + connector_request_reference_id: core_utils::get_connector_request_reference_id( + &state.conf, + &merchant_account.merchant_id, + payment_attempt, + ), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + test_mode, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + }; + Ok(router_data) +} diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs new file mode 100644 index 000000000000..eaefdbefcc77 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -0,0 +1,149 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + connector::signifyd::transformers::RefundMethod, + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FraudCheckConnectorData, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::RecordReturn, + domain, + fraud_check::{ + FraudCheckRecordReturnData, FraudCheckResponseData, FrmRecordReturnRouterData, + }, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + utils, AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> + { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + let currency = self.payment_attempt.clone().currency; + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: utils::OptionExt::get_required_value( + self.payment_attempt.payment_method, + "payment_method_type", + )?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckRecordReturnData { + amount: self.payment_attempt.amount, + refund_method: RefundMethod::OriginalPaymentInstrument, //we dont consume this data now in payments...hence hardcoded + currency, + refund_transaction_id: self.refund.clone().map(|refund| refund.refund_id), + }, // self.order_details + response: Ok(FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + return_id: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmRecordReturnRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmRecordReturnRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + RecordReturn, + FraudCheckRecordReturnData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/sale_flow.rs b/crates/router/src/core/fraud_check/flows/sale_flow.rs new file mode 100644 index 000000000000..c62b096ab374 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/sale_flow.rs @@ -0,0 +1,145 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FraudCheckConnectorData, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{FraudCheckResponseData, FraudCheckSaleData, FrmSaleRouterData}, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckSaleData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + }, + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmSaleRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmSaleRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Sale, + FraudCheckSaleData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/transaction_flow.rs b/crates/router/src/core/fraud_check/flows/transaction_flow.rs new file mode 100644 index 000000000000..1c2b8995dfab --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/transaction_flow.rs @@ -0,0 +1,158 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{ + FraudCheckResponseData, FraudCheckTransactionData, FrmTransactionRouterData, + }, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + frm_api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, + > for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult< + RouterData, + > { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let payment_method = self.payment_attempt.payment_method; + let currency = self.payment_attempt.currency; + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckTransactionData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + currency, + payment_method, + }, // self.order_details + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmTransactionRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &frm_api::FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmTransactionRouterData, + state: &'a AppState, + connector: &frm_api::FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/operation.rs b/crates/router/src/core/fraud_check/operation.rs new file mode 100644 index 000000000000..e7677dad6f3a --- /dev/null +++ b/crates/router/src/core/fraud_check/operation.rs @@ -0,0 +1,106 @@ +pub mod fraud_check_post; +pub mod fraud_check_pre; +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use error_stack::{report, ResultExt}; + +pub use self::{fraud_check_post::FraudCheckPost, fraud_check_pre::FraudCheckPre}; +use super::{ + types::{ConnectorDetailsCore, FrmConfigsObject, PaymentToFrmData}, + FrmData, +}; +use crate::{ + core::{ + errors::{self, RouterResult}, + payments, + }, + db::StorageInterface, + routes::AppState, + types::{domain, fraud_check::FrmRouterData}, +}; + +pub type BoxedFraudCheckOperation = Box + Send + Sync>; + +pub trait FraudCheckOperation: Send + std::fmt::Debug { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("get tracker interface not found for {self:?}")) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("domain interface not found for {self:?}")) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("get tracker interface not found for {self:?}")) + } +} + +#[async_trait] +pub trait GetTracker: Send { + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: D, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult>; +} + +#[async_trait] +pub trait Domain: Send + Sync { + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> + where + F: Send + Clone; + + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult + where + F: Send + Clone; + + // To execute several tasks conditionally based on the result of post_flow. + // Eg: If the /sale(post flow) is returning the transaction as fraud we can execute refund in post task + #[allow(clippy::too_many_arguments)] + async fn execute_post_tasks( + &self, + _state: &AppState, + frm_data: &mut FrmData, + _merchant_account: &domain::MerchantAccount, + _frm_configs: FrmConfigsObject, + _frm_suggestion: &mut Option, + _key_store: domain::MerchantKeyStore, + _payment_data: &mut payments::PaymentData, + _customer: &Option, + ) -> RouterResult> + where + F: Send + Clone, + { + return Ok(Some(frm_data.to_owned())); + } +} + +#[async_trait] +pub trait UpdateTracker: Send { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + frm_data: D, + payment_data: &mut payments::PaymentData, + _frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult; +} diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs new file mode 100644 index 000000000000..37838ddaab5a --- /dev/null +++ b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs @@ -0,0 +1,457 @@ +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use common_utils::ext_traits::Encode; +use data_models::payments::{ + payment_attempt::PaymentAttemptUpdate, payment_intent::PaymentIntentUpdate, +}; +use router_env::{instrument, logger, tracing}; + +use super::{Domain, FraudCheckOperation, GetTracker, UpdateTracker}; +use crate::{ + consts, + core::{ + errors::{RouterResult, StorageErrorExt}, + fraud_check::{ + self as frm_core, + types::{FrmData, PaymentDetails, PaymentToFrmData, REFUND_INITIATED}, + ConnectorDetailsCore, FrmConfigsObject, + }, + payments, refunds, + }, + db::StorageInterface, + errors, services, + types::{ + api::{ + enums::{AttemptStatus, FrmAction, IntentStatus}, + fraud_check as frm_api, + refunds::{RefundRequest, RefundType}, + }, + domain, + fraud_check::{ + FraudCheckResponseData, FraudCheckSaleData, FrmRequest, FrmResponse, FrmRouterData, + }, + storage::{ + enums::{FraudCheckLastStep, FraudCheckStatus, FraudCheckType, MerchantDecision}, + fraud_check::{FraudCheckNew, FraudCheckUpdate}, + }, + ResponseId, + }, + utils, AppState, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FraudCheckPost; + +impl FraudCheckOperation for &FraudCheckPost { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +impl FraudCheckOperation for FraudCheckPost { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +#[async_trait] +impl GetTracker for FraudCheckPost { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: PaymentToFrmData, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult> { + let db = &*state.store; + + let payment_details: Option = + Encode::::encode_to_value(&PaymentDetails::from(payment_data.clone())) + .ok(); + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + payment_data.payment_intent.payment_id.clone(), + payment_data.merchant_account.merchant_id.clone(), + ) + .await + .ok(); + let fraud_check = match existing_fraud_check { + Some(Some(fraud_check)) => Ok(fraud_check), + _ => { + db.insert_fraud_check_response(FraudCheckNew { + frm_id: utils::generate_id(consts::ID_LENGTH, "frm"), + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: payment_data.merchant_account.merchant_id.clone(), + attempt_id: payment_data.payment_attempt.attempt_id.clone(), + created_at: common_utils::date_time::now(), + frm_name: frm_connector_details.connector_name, + frm_transaction_id: None, + frm_transaction_type: FraudCheckType::PostFrm, + frm_status: FraudCheckStatus::Pending, + frm_score: None, + frm_reason: None, + frm_error: None, + payment_details, + metadata: None, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Processing, + }) + .await + } + }; + match fraud_check { + Ok(fraud_check_value) => { + let frm_data = FrmData { + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: payment_data.merchant_account, + address: payment_data.address, + fraud_check: fraud_check_value, + connector_details: payment_data.connector_details, + order_details: payment_data.order_details, + refund: None, + }; + Ok(Some(frm_data)) + } + Err(error) => { + router_env::logger::error!("inserting into fraud_check table failed {error:?}"); + Ok(None) + } + } + } +} + +#[async_trait] +impl Domain for FraudCheckPost { + #[instrument(skip_all)] + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> { + if frm_data.fraud_check.last_step != FraudCheckLastStep::Processing { + logger::debug!("post_flow::Sale Skipped"); + return Ok(None); + } + let router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::CheckoutOrSale; + Ok(Some(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Sale(FraudCheckSaleData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Sale(router_data.response), + })) + } + + #[instrument(skip_all)] + async fn execute_post_tasks( + &self, + state: &AppState, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + frm_configs: FrmConfigsObject, + frm_suggestion: &mut Option, + key_store: domain::MerchantKeyStore, + payment_data: &mut payments::PaymentData, + customer: &Option, + ) -> RouterResult> { + if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Fraud) + && matches!(frm_configs.frm_action, FrmAction::AutoRefund) + && matches!( + frm_data.fraud_check.last_step, + FraudCheckLastStep::CheckoutOrSale + ) + { + *frm_suggestion = Some(FrmSuggestion::FrmAutoRefund); + let ref_req = RefundRequest { + refund_id: None, + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: Some(merchant_account.merchant_id.clone()), + amount: None, + reason: frm_data + .fraud_check + .frm_reason + .clone() + .map(|data| data.to_string()), + refund_type: Some(RefundType::Instant), + metadata: None, + merchant_connector_details: None, + }; + let refund = Box::pin(refunds::refund_create_core( + state.clone(), + merchant_account.clone(), + key_store.clone(), + ref_req, + )) + .await?; + if let services::ApplicationResponse::Json(new_refund) = refund { + frm_data.refund = Some(new_refund); + } + let _router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund; + }; + return Ok(Some(frm_data.to_owned())); + } + + #[instrument(skip_all)] + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + Ok(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Sale(FraudCheckSaleData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Sale(router_data.response), + }) + } +} + +#[async_trait] +impl UpdateTracker for FraudCheckPost { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + mut frm_data: FrmData, + payment_data: &mut payments::PaymentData, + frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult { + let frm_check_update = match frm_router_data.response { + FrmResponse::Sale(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + }, + FraudCheckResponseData::RecordReturnResponse { resource_id: _, connector_metadata: _, return_id: _ } => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Sale flow".to_string(), + )), + }) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + }, + }, + FrmResponse::Fulfillment(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { resource_id: _, connector_metadata: _, return_id: _ } => None, + + }, + }, + + FrmResponse::RecordReturn(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id: _, + connector_metadata: _, + status: _, + reason: _, + score: _, + } => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Transaction Response response in current Record Return flow".to_string(), + )), + }) + }, + FraudCheckResponseData::FulfillmentResponse {order_id: _, shipment_ids: _ } => { + None + }, + FraudCheckResponseData::RecordReturnResponse { resource_id, connector_metadata, return_id: _ } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: frm_data.fraud_check.frm_status, + frm_transaction_id: connector_transaction_id, + frm_reason: frm_data.fraud_check.frm_reason.clone(), + frm_score: frm_data.fraud_check.frm_score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + + } + }, + }, + + + FrmResponse::Checkout(_) | FrmResponse::Transaction(_) => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Pre(Sale) flow response in current post flow".to_string(), + )), + }) + } + }; + + if frm_suggestion == Some(FrmSuggestion::FrmAutoRefund) { + payment_data.payment_attempt = db + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + PaymentAttemptUpdate::RejectUpdate { + status: AttemptStatus::Failure, + error_code: Some(Some(frm_data.fraud_check.frm_status.to_string())), + error_message: Some(Some(REFUND_INITIATED.to_string())), + updated_by: frm_data.merchant_account.storage_scheme.to_string(), // merchant_decision: Some(MerchantDecision::AutoRefunded), + }, + frm_data.merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent.clone(), + PaymentIntentUpdate::RejectUpdate { + status: IntentStatus::Failed, + merchant_decision: Some(MerchantDecision::AutoRefunded.to_string()), + updated_by: frm_data.merchant_account.storage_scheme.to_string(), + }, + frm_data.merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + frm_data.fraud_check = match frm_check_update { + Some(fraud_check_update) => db + .update_fraud_check_response_with_attempt_id( + frm_data.fraud_check.clone(), + fraud_check_update, + ) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?, + None => frm_data.fraud_check.clone(), + }; + + Ok(frm_data) + } +} diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs new file mode 100644 index 000000000000..00f50d01a862 --- /dev/null +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -0,0 +1,337 @@ +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use common_utils::ext_traits::Encode; +use diesel_models::enums::FraudCheckLastStep; +use router_env::{instrument, tracing}; +use uuid::Uuid; + +use super::{Domain, FraudCheckOperation, GetTracker, UpdateTracker}; +use crate::{ + core::{ + errors::RouterResult, + fraud_check::{ + self as frm_core, + types::{FrmData, PaymentDetails, PaymentToFrmData}, + ConnectorDetailsCore, + }, + payments, + }, + db::StorageInterface, + errors, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{ + FraudCheckCheckoutData, FraudCheckResponseData, FraudCheckTransactionData, FrmRequest, + FrmResponse, FrmRouterData, + }, + storage::{ + enums::{FraudCheckStatus, FraudCheckType}, + fraud_check::{FraudCheckNew, FraudCheckUpdate}, + }, + ResponseId, + }, + AppState, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FraudCheckPre; + +impl FraudCheckOperation for &FraudCheckPre { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +impl FraudCheckOperation for FraudCheckPre { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +#[async_trait] +impl GetTracker for FraudCheckPre { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: PaymentToFrmData, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult> { + let db = &*state.store; + + let payment_details: Option = + Encode::::encode_to_value(&PaymentDetails::from(payment_data.clone())) + .ok(); + + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + payment_data.payment_intent.payment_id.clone(), + payment_data.merchant_account.merchant_id.clone(), + ) + .await + .ok(); + + let fraud_check = match existing_fraud_check { + Some(Some(fraud_check)) => Ok(fraud_check), + _ => { + db.insert_fraud_check_response(FraudCheckNew { + frm_id: Uuid::new_v4().simple().to_string(), + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: payment_data.merchant_account.merchant_id.clone(), + attempt_id: payment_data.payment_attempt.attempt_id.clone(), + created_at: common_utils::date_time::now(), + frm_name: frm_connector_details.connector_name, + frm_transaction_id: None, + frm_transaction_type: FraudCheckType::PreFrm, + frm_status: FraudCheckStatus::Pending, + frm_score: None, + frm_reason: None, + frm_error: None, + payment_details, + metadata: None, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Processing, + }) + .await + } + }; + + match fraud_check { + Ok(fraud_check_value) => { + let frm_data = FrmData { + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: payment_data.merchant_account, + address: payment_data.address, + fraud_check: fraud_check_value, + connector_details: payment_data.connector_details, + order_details: payment_data.order_details, + refund: None, + }; + Ok(Some(frm_data)) + } + Err(error) => { + router_env::logger::error!("inserting into fraud_check table failed {error:?}"); + Ok(None) + } + } + } +} + +#[async_trait] +impl Domain for FraudCheckPre { + #[instrument(skip_all)] + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund; + Ok(Some(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Transaction(FraudCheckTransactionData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + currency: router_data.request.currency, + payment_method: Some(router_data.payment_method), + }), + response: FrmResponse::Transaction(router_data.response), + })) + } + + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::CheckoutOrSale; + Ok(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Checkout(FraudCheckCheckoutData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Checkout(router_data.response), + }) + } +} + +#[async_trait] +impl UpdateTracker for FraudCheckPre { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + mut frm_data: FrmData, + payment_data: &mut payments::PaymentData, + _frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult { + let frm_check_update = match frm_router_data.response { + FrmResponse::Checkout(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { + resource_id: _, + connector_metadata: _, + return_id: _, + } => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Checkout flow" + .to_string(), + )), + }), + }, + }, + FrmResponse::Transaction(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let frm_status = payment_data + .frm_message + .as_ref() + .map_or(status, |frm_data| frm_data.frm_status); + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { + resource_id: _, + connector_metadata: _, + return_id: _, + } => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Checkout flow" + .to_string(), + )), + }), + }, + }, + FrmResponse::Sale(_response) + | FrmResponse::Fulfillment(_response) + | FrmResponse::RecordReturn(_response) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Pre(Sale) flow response in current post flow".to_string(), + )), + }), + }; + + frm_data.fraud_check = match frm_check_update { + Some(fraud_check_update) => db + .update_fraud_check_response_with_attempt_id( + frm_data.clone().fraud_check, + fraud_check_update, + ) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?, + None => frm_data.clone().fraud_check, + }; + + Ok(frm_data) + } +} diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs new file mode 100644 index 000000000000..1d6e7cb45a58 --- /dev/null +++ b/crates/router/src/core/fraud_check/types.rs @@ -0,0 +1,208 @@ +use api_models::{ + enums as api_enums, + enums::{PaymentMethod, PaymentMethodType}, + payments::Amount, + refunds::RefundResponse, +}; +use common_enums::FrmSuggestion; +use common_utils::pii::Email; +use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use masking::Serialize; +use serde::Deserialize; +use utoipa::ToSchema; + +use super::operation::BoxedFraudCheckOperation; +use crate::{ + pii::Secret, + types::{ + domain::MerchantAccount, + storage::{enums as storage_enums, fraud_check::FraudCheck}, + PaymentAddress, + }, +}; + +#[derive(Clone, Default, Debug)] +pub struct PaymentIntentCore { + pub payment_id: String, +} + +#[derive(Clone, Debug)] +pub struct PaymentAttemptCore { + pub attempt_id: String, + pub payment_details: Option, + pub amount: Amount, +} + +#[derive(Clone, Debug, Serialize)] +pub struct PaymentDetails { + pub amount: i64, + pub currency: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub refund_transaction_id: Option, +} +#[derive(Clone, Default, Debug)] +pub struct FrmMerchantAccount { + pub merchant_id: String, +} + +#[derive(Clone, Debug)] +pub struct FrmData { + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, + pub merchant_account: MerchantAccount, + pub fraud_check: FraudCheck, + pub address: PaymentAddress, + pub connector_details: ConnectorDetailsCore, + pub order_details: Option>, + pub refund: Option, +} + +#[derive(Debug)] +pub struct FrmInfo { + pub fraud_check_operation: BoxedFraudCheckOperation, + pub frm_data: Option, + pub suggested_action: Option, +} + +#[derive(Clone, Debug)] +pub struct ConnectorDetailsCore { + pub connector_name: String, + pub profile_id: String, +} +#[derive(Clone)] +pub struct PaymentToFrmData { + pub amount: Amount, + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, + pub merchant_account: MerchantAccount, + pub address: PaymentAddress, + pub connector_details: ConnectorDetailsCore, + pub order_details: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FrmConfigsObject { + pub frm_enabled_pm: Option, + pub frm_enabled_pm_type: Option, + pub frm_enabled_gateway: Option, + pub frm_action: api_enums::FrmAction, + pub frm_preferred_flow_type: api_enums::FrmPreferredFlowTypes, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFulfillmentSignifydApiRequest { + ///unique order_id for the order_details in the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///denotes the status of the fulfillment... can be one of PARTIAL, COMPLETE, REPLACEMENT, CANCELED + #[schema(value_type = Option, example = "COMPLETE")] + pub fulfillment_status: Option, + ///contains details of the fulfillment + #[schema(value_type = Vec)] + pub fulfillments: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct FrmFulfillmentRequest { + ///unique payment_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub payment_id: String, + ///unique order_id for the order_details in the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///denotes the status of the fulfillment... can be one of PARTIAL, COMPLETE, REPLACEMENT, CANCELED + #[schema(value_type = Option, example = "COMPLETE")] + pub fulfillment_status: Option, + ///contains details of the fulfillment + #[schema(value_type = Vec)] + pub fulfillments: Vec, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Fulfillments { + ///shipment_id of the shipped items + #[schema(max_length = 255, example = "ship_101")] + pub shipment_id: String, + ///products sent in the shipment + #[schema(value_type = Option>)] + pub products: Option>, + ///destination address of the shipment + #[schema(value_type = Destination)] + pub destination: Destination, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Destination { + pub full_name: Secret, + pub organization: Option, + pub email: Option, + pub address: Address, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Address { + pub street_address: Secret, + pub unit: Option>, + pub postal_code: Secret, + pub city: String, + pub province_code: Secret, + pub country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, ToSchema, Clone, Serialize)] +pub struct FrmFulfillmentResponse { + ///unique order_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///shipment_ids used in the fulfillment overall...also data from previous fulfillments for the same transactions/order is sent + #[schema(example = r#"["ship_101", "ship_102"]"#)] + pub shipment_ids: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFulfillmentSignifydApiResponse { + ///unique order_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///shipment_ids used in the fulfillment overall...also data from previous fulfillments for the same transactions/order is sent + #[schema(example = r#"["ship_101","ship_102"]"#)] + pub shipment_ids: Vec, +} + +pub const REFUND_INITIATED: &str = "Refund Initiated with the processor"; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 21a2866c9f4e..73af17f9d66b 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -41,6 +41,8 @@ use self::{ routing::{self as self_routing, SessionFlowRoutingInput}, }; use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs}; +#[cfg(feature = "frm")] +use crate::core::fraud_check as frm_core; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -170,154 +172,231 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { - payment_data = match connector_details { - api::ConnectorCallType::PreDetermined(connector) => { - let schedule_time = if should_add_task_to_process_tracker { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector.connector.id(), - &merchant_account.merchant_id, - 0, - ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - }; - let router_data = call_connector_service( - state, - &merchant_account, - &key_store, - connector, - &operation, - &mut payment_data, - &customer, - call_connector_action, - &validate_result, - schedule_time, - header_payload, - ) + // Fetch and check FRM configs + #[cfg(feature = "frm")] + let mut frm_info = None; + #[cfg(feature = "frm")] + let db = &*state.store; + #[allow(unused_variables, unused_mut)] + let mut should_continue_transaction: bool = true; + #[cfg(feature = "frm")] + let frm_configs = if state.conf.frm.enabled { + frm_core::call_frm_before_connector_call( + db, + &operation, + &merchant_account, + &mut payment_data, + state, + &mut frm_info, + &customer, + &mut should_continue_transaction, + key_store.clone(), + ) + .await? + } else { + None + }; + #[cfg(feature = "frm")] + logger::debug!( + "should_cancel_transaction: {:?} {:?} ", + frm_configs, + should_continue_transaction + ); + + if should_continue_transaction { + operation + .to_domain()? + .populate_payment_data(state, &mut payment_data, &merchant_account) .await?; - let operation = Box::new(PaymentResponse); - - connector_http_status_code = router_data.connector_http_status_code; - external_latency = router_data.external_latency; - //add connector http status code metrics - add_connector_http_status_code_metrics(connector_http_status_code); - operation - .to_post_update_tracker()? - .update_tracker( + payment_data = match connector_details { + api::ConnectorCallType::PreDetermined(connector) => { + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( state, - &validate_result.payment_id, - payment_data, - router_data, - merchant_account.storage_scheme, + &merchant_account, + &key_store, + connector, + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, ) - .await? - } + .await?; + let operation = Box::new(PaymentResponse); + + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( + state, + &validate_result.payment_id, + payment_data, + router_data, + merchant_account.storage_scheme, + ) + .await? + } - api::ConnectorCallType::Retryable(connectors) => { - let mut connectors = connectors.into_iter(); + api::ConnectorCallType::Retryable(connectors) => { + let mut connectors = connectors.into_iter(); - let connector_data = get_connector_data(&mut connectors)?; + let connector_data = get_connector_data(&mut connectors)?; - let schedule_time = if should_add_task_to_process_tracker { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector_data.connector.id(), - &merchant_account.merchant_id, - 0, + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector_data.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( + state, + &merchant_account, + &key_store, + connector_data.clone(), + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - }; - let router_data = call_connector_service( - state, - &merchant_account, - &key_store, - connector_data.clone(), - &operation, - &mut payment_data, - &customer, - call_connector_action, - &validate_result, - schedule_time, - header_payload, - ) - .await?; + .await?; - #[cfg(feature = "retry")] - let mut router_data = router_data; - #[cfg(feature = "retry")] - { - use crate::core::payments::retry::{self, GsmValidation}; - let config_bool = - retry::config_should_call_gsm(&*state.store, &merchant_account.merchant_id) - .await; + #[cfg(feature = "retry")] + let mut router_data = router_data; + #[cfg(feature = "retry")] + { + use crate::core::payments::retry::{self, GsmValidation}; + let config_bool = retry::config_should_call_gsm( + &*state.store, + &merchant_account.merchant_id, + ) + .await; + + if config_bool && router_data.should_call_gsm() { + router_data = retry::do_gsm_actions( + state, + &mut payment_data, + connectors, + connector_data, + router_data, + &merchant_account, + &key_store, + &operation, + &customer, + &validate_result, + schedule_time, + ) + .await?; + }; + } - if config_bool && router_data.should_call_gsm() { - router_data = retry::do_gsm_actions( + let operation = Box::new(PaymentResponse); + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( state, - &mut payment_data, - connectors, - connector_data, + &validate_result.payment_id, + payment_data, router_data, - &merchant_account, - &key_store, - &operation, - &customer, - &validate_result, - schedule_time, + merchant_account.storage_scheme, ) - .await?; - }; + .await? } - let operation = Box::new(PaymentResponse); - connector_http_status_code = router_data.connector_http_status_code; - external_latency = router_data.external_latency; - //add connector http status code metrics - add_connector_http_status_code_metrics(connector_http_status_code); - operation - .to_post_update_tracker()? - .update_tracker( + api::ConnectorCallType::SessionMultiple(connectors) => { + let session_surcharge_details = + call_surcharge_decision_management_for_session_flow( + state, + &merchant_account, + &mut payment_data, + &connectors, + ) + .await?; + call_multiple_connectors_service( state, - &validate_result.payment_id, + &merchant_account, + &key_store, + connectors, + &operation, payment_data, - router_data, - merchant_account.storage_scheme, + &customer, + session_surcharge_details, ) .await? - } + } + }; - api::ConnectorCallType::SessionMultiple(connectors) => { - let session_surcharge_details = - call_surcharge_decision_management_for_session_flow( - state, - &merchant_account, - &mut payment_data, - &connectors, - ) - .await?; - call_multiple_connectors_service( + #[cfg(feature = "frm")] + if let Some(fraud_info) = &mut frm_info { + Box::pin(frm_core::post_payment_frm_core( state, &merchant_account, - &key_store, - connectors, - &operation, - payment_data, + &mut payment_data, + fraud_info, + frm_configs + .clone() + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_configs", + }) + .into_report() + .attach_printable("Frm configs label not found")?, &customer, - session_surcharge_details, - ) - .await? + key_store, + )) + .await?; } - }; + } else { + (_, payment_data) = operation + .to_update_tracker()? + .update_trackers( + state, + payment_data.clone(), + customer.clone(), + validate_result.storage_scheme, + None, + &key_store, + #[cfg(feature = "frm")] + frm_info.and_then(|info| info.suggested_action), + #[cfg(not(feature = "frm"))] + None, + header_payload, + ) + .await?; + } + payment_data .payment_attempt .payment_token diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 94b8bc1ff5d4..81ba48e9831f 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -11,6 +11,8 @@ pub mod setup_mandate_flow; use async_trait::async_trait; +#[cfg(feature = "frm")] +use crate::types::fraud_check as frm_types; use crate::{ connector, core::{ @@ -170,6 +172,7 @@ default_imp_for_complete_authorize!( connector::Payeezy, connector::Payu, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -247,6 +250,7 @@ default_imp_for_webhook_source_verification!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -326,6 +330,7 @@ default_imp_for_create_customer!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Trustpay, connector::Tsys, @@ -394,6 +399,7 @@ default_imp_for_connector_redirect_response!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Tsys, @@ -453,6 +459,7 @@ default_imp_for_connector_request_id!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -535,6 +542,7 @@ default_imp_for_accept_dispute!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -635,6 +643,7 @@ default_imp_for_file_upload!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Trustpay, @@ -713,6 +722,7 @@ default_imp_for_submit_evidence!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Trustpay, @@ -791,6 +801,7 @@ default_imp_for_defend_dispute!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -868,6 +879,7 @@ default_imp_for_pre_processing_steps!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Tsys, @@ -928,6 +940,7 @@ default_imp_for_payouts!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1007,6 +1020,7 @@ default_imp_for_payouts_create!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1089,6 +1103,7 @@ default_imp_for_payouts_eligibility!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1168,6 +1183,7 @@ default_imp_for_payouts_fulfill!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1247,6 +1263,7 @@ default_imp_for_payouts_cancel!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1327,6 +1344,7 @@ default_imp_for_payouts_quote!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1407,6 +1425,7 @@ default_imp_for_payouts_recipient!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1486,6 +1505,7 @@ default_imp_for_approve!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1527,6 +1547,481 @@ impl } default_imp_for_reject!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Signifyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_fraud_check { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheck for $path::$connector {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheck for connector::DummyConnector {} + +default_imp_for_fraud_check!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_sale { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckSale for $path::$connector {} + impl + services::ConnectorIntegration< + api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckSale for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_sale!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_checkout { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckCheckout for $path::$connector {} + impl + services::ConnectorIntegration< + api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckCheckout for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_checkout!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_transaction { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckTransaction for $path::$connector {} + impl + services::ConnectorIntegration< + api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckTransaction for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_transaction!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_fulfillment { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckFulfillment for $path::$connector {} + impl + services::ConnectorIntegration< + api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckFulfillment for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_fulfillment!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +#[cfg(feature = "frm")] +macro_rules! default_imp_for_frm_record_return { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckRecordReturn for $path::$connector {} + impl + services::ConnectorIntegration< + api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl api::FraudCheckRecordReturn for connector::DummyConnector {} +#[cfg(all(feature = "frm", feature = "dummy_connector"))] +impl + services::ConnectorIntegration< + api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +#[cfg(feature = "frm")] +default_imp_for_frm_record_return!( connector::Aci, connector::Adyen, connector::Airwallex, @@ -1645,6 +2140,7 @@ default_imp_for_incremental_authorization!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 9b3006692d34..ce1717c9e936 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -14,6 +14,8 @@ pub mod disputes; pub mod dummy_connector; pub mod ephemeral_key; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod gsm; pub mod health; pub mod lock_utils; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 9739d18864b8..6b72e69b9f4e 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -34,6 +34,8 @@ use super::{cache::*, health::*}; use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; +#[cfg(all(feature = "frm", feature = "oltp"))] +use crate::routes::fraud_check as frm_routes; #[cfg(feature = "olap")] use crate::routes::verify_connector::payment_connector_verify; pub use crate::{ @@ -334,6 +336,14 @@ impl Payments { .service( web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)), ) + .service( + web::resource("/{payment_id}/approve") + .route(web::post().to(payments_approve)), + ) + .service( + web::resource("/{payment_id}/reject") + .route(web::post().to(payments_reject)), + ) .service( web::resource("/redirect/{payment_id}/{merchant_id}/{attempt_id}") .route(web::get().to(payments_start)), @@ -650,7 +660,8 @@ impl Webhooks { pub fn server(config: AppState) -> Scope { use api_models::webhooks as webhook_type; - web::scope("/webhooks") + #[allow(unused_mut)] + let mut route = web::scope("/webhooks") .app_data(web::Data::new(config)) .service( web::resource("/{merchant_id}/{connector_id_or_name}") @@ -661,7 +672,17 @@ impl Webhooks { .route( web::put().to(receive_incoming_webhook::), ), - ) + ); + + #[cfg(feature = "frm")] + { + route = route.service( + web::resource("/frm_fulfillment") + .route(web::post().to(frm_routes::frm_fulfillment)), + ); + } + + route } } diff --git a/crates/router/src/routes/fraud_check.rs b/crates/router/src/routes/fraud_check.rs new file mode 100644 index 000000000000..d4363a236bb3 --- /dev/null +++ b/crates/router/src/routes/fraud_check.rs @@ -0,0 +1,42 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use common_utils::events::{ApiEventMetric, ApiEventsType}; +use router_env::Flow; + +use crate::{ + core::{api_locking, fraud_check as frm_core}, + services::{self, api}, + types::fraud_check::FraudCheckResponseData, + AppState, +}; + +pub async fn frm_fulfillment( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::FrmFulfillment; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth, req| { + frm_core::frm_fulfillment_core(state, auth.merchant_account, auth.key_store, req) + }, + &services::authentication::ApiKeyAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +impl ApiEventMetric for FraudCheckResponseData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::FraudCheck) + } +} + +impl ApiEventMetric for frm_core::types::FrmFulfillmentRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::FraudCheck) + } +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index dcae11f58b76..88c35bb0a13d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -112,7 +112,7 @@ impl From for ApiIdentifier { | Flow::RefundsUpdate | Flow::RefundsList => Self::Refunds, - Flow::IncomingWebhookReceive => Self::Webhooks, + Flow::FrmFulfillment | Flow::IncomingWebhookReceive => Self::Webhooks, Flow::ApiKeyCreate | Flow::ApiKeyRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index e424e93c78ed..b836f02cded2 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -907,6 +907,122 @@ pub async fn get_filters_for_payments( ) .await } + +#[cfg(feature = "oltp")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsApprove, payment_id))] +// #[post("/{payment_id}/approve")] +pub async fn payments_approve( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; + let flow = Flow::PaymentsApprove; + let fpayload = FPaymentsApproveRequest(&payload); + let locking_action = fpayload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.clone(), + |state, auth, req| { + payments::payments_core::< + api_types::Authorize, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentApprove, + payment_types::PaymentsRequest { + payment_id: Some(payment_types::PaymentIdType::PaymentIntentId( + req.payment_id, + )), + ..Default::default() + }, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + payment_types::HeaderPayload::default(), + ) + }, + match env::which() { + env::Env::Production => &auth::ApiKeyAuth, + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + http_req.headers(), + ), + }, + locking_action, + )) + .await +} + +#[cfg(feature = "oltp")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsReject, payment_id))] +// #[post("/{payment_id}/reject")] +pub async fn payments_reject( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; + let flow = Flow::PaymentsReject; + let fpayload = FPaymentsRejectRequest(&payload); + let locking_action = fpayload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.clone(), + |state, auth, req| { + payments::payments_core::< + api_types::Reject, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentReject, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + payment_types::HeaderPayload::default(), + ) + }, + match env::which() { + env::Env::Production => &auth::ApiKeyAuth, + _ => auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::PaymentWrite), + http_req.headers(), + ), + }, + locking_action, + )) + .await +} + async fn authorize_verify_select( operation: Op, state: app::AppState, @@ -1197,6 +1313,42 @@ impl GetLockingInput for payment_types::PaymentsCaptureRequest { } } +struct FPaymentsApproveRequest<'a>(&'a payment_types::PaymentsApproveRequest); + +impl<'a> GetLockingInput for FPaymentsApproveRequest<'a> { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.0.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + +struct FPaymentsRejectRequest<'a>(&'a payment_types::PaymentsRejectRequest); + +impl<'a> GetLockingInput for FPaymentsRejectRequest<'a> { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.0.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + impl GetLockingInput for payment_types::PaymentsIncrementalAuthorizationRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction where diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index ee5727bbda90..918aab929ac9 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1191,6 +1191,8 @@ impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} impl Authenticate for api_models::payments::PaymentsIncrementalAuthorizationRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} +// impl Authenticate for api_models::payments::PaymentsApproveRequest {} +impl Authenticate for api_models::payments::PaymentsRejectRequest {} pub fn build_redirection_form( form: &RedirectForm, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 08cbb36952e3..de28c1a3188c 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -8,6 +8,8 @@ pub mod api; pub mod domain; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod storage; pub mod transformers; @@ -22,6 +24,7 @@ use common_utils::{pii, pii::Email}; use data_models::mandates::MandateData; use error_stack::{IntoReport, ResultExt}; use masking::Secret; +use serde::Serialize; use self::{api::payments, storage::enums as storage_enums}; pub use crate::core::payments::{CustomerDetails, PaymentAddress}; @@ -741,7 +744,7 @@ pub enum PreprocessingResponseId { ConnectorTransactionId(String), } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub enum ResponseId { ConnectorTransactionId(String), EncodedData(String), diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 0ec158199cea..978ce078faf9 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -8,6 +8,8 @@ pub mod disputes; pub mod enums; pub mod ephemeral_key; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod mandates; pub mod payment_link; pub mod payment_methods; @@ -23,6 +25,8 @@ use std::{fmt::Debug, str::FromStr}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "frm")] +pub use self::fraud_check::*; pub use self::{ admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, @@ -154,6 +158,7 @@ pub trait Connector: + ConnectorTransactionId + Payouts + ConnectorVerifyWebhookSource + + FraudCheck { } @@ -173,7 +178,8 @@ impl< + FileUpload + ConnectorTransactionId + Payouts - + ConnectorVerifyWebhookSource, + + ConnectorVerifyWebhookSource + + FraudCheck, > Connector for T { } @@ -412,6 +418,20 @@ impl ConnectorData { } } +#[cfg(feature = "frm")] +pub trait FraudCheck: + ConnectorCommon + + FraudCheckSale + + FraudCheckTransaction + + FraudCheckCheckout + + FraudCheckFulfillment + + FraudCheckRecordReturn +{ +} + +#[cfg(not(feature = "frm"))] +pub trait FraudCheck {} + #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs new file mode 100644 index 000000000000..7be60bfee952 --- /dev/null +++ b/crates/router/src/types/api/fraud_check.rs @@ -0,0 +1,91 @@ +use std::str::FromStr; + +use api_models::enums; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use super::{BoxedConnector, ConnectorData, SessionConnectorData}; +use crate::{ + connector, + core::errors, + services::api, + types::fraud_check::{ + FraudCheckCheckoutData, FraudCheckFulfillmentData, FraudCheckRecordReturnData, + FraudCheckResponseData, FraudCheckSaleData, FraudCheckTransactionData, + }, +}; + +#[derive(Debug, Clone)] +pub struct Sale; + +pub trait FraudCheckSale: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Checkout; + +pub trait FraudCheckCheckout: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Transaction; + +pub trait FraudCheckTransaction: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Fulfillment; + +pub trait FraudCheckFulfillment: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct RecordReturn; + +pub trait FraudCheckRecordReturn: + api::ConnectorIntegration +{ +} + +#[derive(Clone, Debug)] +pub struct FraudCheckConnectorData { + pub connector: BoxedConnector, + pub connector_name: enums::FrmConnectors, +} +pub enum ConnectorCallType { + PreDetermined(ConnectorData), + Retryable(Vec), + SessionMultiple(Vec), +} + +impl FraudCheckConnectorData { + pub fn get_connector_by_name(name: &str) -> CustomResult { + let connector_name = enums::FrmConnectors::from_str(name) + .into_report() + .change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven) + .attach_printable_lazy(|| { + format!("unable to parse connector: {:?}", name.to_string()) + })?; + let connector = Self::convert_connector(connector_name)?; + Ok(Self { + connector, + connector_name, + }) + } + + fn convert_connector( + connector_name: enums::FrmConnectors, + ) -> CustomResult { + match connector_name { + enums::FrmConnectors::Signifyd => Ok(Box::new(&connector::Signifyd)), + } + } +} diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs new file mode 100644 index 000000000000..4bbba8ac4dca --- /dev/null +++ b/crates/router/src/types/fraud_check.rs @@ -0,0 +1,126 @@ +use crate::{ + connector::signifyd::transformers::{FrmFullfillmentSignifydApiRequest, RefundMethod}, + pii::Serialize, + services, + types::{api, storage_enums, ErrorResponse, ResponseId, RouterData}, +}; +pub type FrmSaleRouterData = RouterData; + +pub type FrmSaleType = + dyn services::ConnectorIntegration; + +#[derive(Debug, Clone)] +pub struct FraudCheckSaleData { + pub amount: i64, + pub order_details: Option>, +} +#[derive(Debug, Clone)] +pub struct FrmRouterData { + pub merchant_id: String, + pub connector: String, + pub payment_id: String, + pub attempt_id: String, + pub request: FrmRequest, + pub response: FrmResponse, +} +#[derive(Debug, Clone)] +pub enum FrmRequest { + Sale(FraudCheckSaleData), + Checkout(FraudCheckCheckoutData), + Transaction(FraudCheckTransactionData), + Fulfillment(FraudCheckFulfillmentData), + RecordReturn(FraudCheckRecordReturnData), +} +#[derive(Debug, Clone)] +pub enum FrmResponse { + Sale(Result), + Checkout(Result), + Transaction(Result), + Fulfillment(Result), + RecordReturn(Result), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum FraudCheckResponseData { + TransactionResponse { + resource_id: ResponseId, + status: storage_enums::FraudCheckStatus, + connector_metadata: Option, + reason: Option, + score: Option, + }, + FulfillmentResponse { + order_id: String, + shipment_ids: Vec, + }, + RecordReturnResponse { + resource_id: ResponseId, + connector_metadata: Option, + return_id: Option, + }, +} + +pub type FrmCheckoutRouterData = + RouterData; + +pub type FrmCheckoutType = dyn services::ConnectorIntegration< + api::Checkout, + FraudCheckCheckoutData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckCheckoutData { + pub amount: i64, + pub order_details: Option>, +} + +pub type FrmTransactionRouterData = + RouterData; + +pub type FrmTransactionType = dyn services::ConnectorIntegration< + api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckTransactionData { + pub amount: i64, + pub order_details: Option>, + pub currency: Option, + pub payment_method: Option, +} + +pub type FrmFulfillmentRouterData = + RouterData; + +pub type FrmFulfillmentType = dyn services::ConnectorIntegration< + api::Fulfillment, + FraudCheckFulfillmentData, + FraudCheckResponseData, +>; +pub type FrmRecordReturnRouterData = + RouterData; + +pub type FrmRecordReturnType = dyn services::ConnectorIntegration< + api::RecordReturn, + FraudCheckRecordReturnData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckFulfillmentData { + pub amount: i64, + pub order_details: Option>>, + pub fulfillment_request: FrmFullfillmentSignifydApiRequest, +} + +#[derive(Debug, Clone)] +pub struct FraudCheckRecordReturnData { + pub amount: i64, + pub currency: Option, + pub refund_method: RefundMethod, + pub refund_transaction_id: Option, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index c8cc7f9c010f..1dc241cde20c 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -12,6 +12,7 @@ pub mod enums; pub mod ephemeral_key; pub mod events; pub mod file; +pub mod fraud_check; pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; @@ -23,30 +24,29 @@ pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_link; pub mod payment_method; -pub mod routing_algorithm; -use std::collections::HashMap; - -pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; -pub use scheduler::db::process_tracker; -pub mod reverse_lookup; - pub mod payout_attempt; pub mod payouts; mod query; pub mod refund; +pub mod reverse_lookup; +pub mod routing_algorithm; pub mod user; pub mod user_role; +use std::collections::HashMap; + pub use data_models::payments::{ payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, PaymentIntent, }; +pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; +pub use scheduler::db::process_tracker; pub use self::{ address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, - dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, - locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, + dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, + gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; diff --git a/crates/router/src/types/storage/fraud_check.rs b/crates/router/src/types/storage/fraud_check.rs new file mode 100644 index 000000000000..f3dd259c3ce4 --- /dev/null +++ b/crates/router/src/types/storage/fraud_check.rs @@ -0,0 +1,3 @@ +pub use diesel_models::fraud_check::{ + FraudCheck, FraudCheckNew, FraudCheckUpdate, FraudCheckUpdateInternal, +}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 0244d8dc18ef..34ae3dceb5ab 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -760,7 +760,7 @@ impl TryFrom for api_models::admin::MerchantCo .parse_value("FrmConfigs") .change_context(errors::ApiErrorResponse::InvalidDataFormat { field_name: "frm_configs".to_string(), - expected_format: "[{ \"gateway\": \"stripe\", \"payment_methods\": [{ \"payment_method\": \"card\",\"payment_method_types\": [{\"payment_method_type\": \"credit\",\"card_networks\": [\"Visa\"],\"flow\": \"pre\",\"action\": \"cancel_txn\"}]}]}]".to_string(), + expected_format: r#"[{ "gateway": "stripe", "payment_methods": [{ "payment_method": "card","payment_method_types": [{"payment_method_type": "credit","card_networks": ["Visa"],"flow": "pre","action": "cancel_txn"}]}]}]"#.to_string(), }) }) .collect::, _>>()?; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 4948bdd575b3..13ca344e9c57 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -263,6 +263,8 @@ pub enum Flow { DecisionManagerDeleteConfig, /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, + /// Manual payment fulfillment acknowledgement + FrmFulfillment, /// Change password flow ChangePassword, /// Set Dashboard Metadata flow diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 2159d2d7994f..788835dd29de 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -115,6 +115,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" @@ -263,8 +264,10 @@ connection_timeout = 10 [kv_config] ttl = 300 # 5 * 60 seconds +[frm] +enabled = true + [connector_onboarding.paypal] client_id = "" client_secret = "" partner_id = "" -enabled = true From 751f9e5ba214d8a39b160e0dde9e2505e50a73a4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:58:44 +0000 Subject: [PATCH 151/443] chore(version): v1.96.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd968293c4b..e87adeea9357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.96.0 (2023-12-05) + +### Features + +- **connector_onboarding:** Add Connector onboarding APIs ([#3050](https://github.com/juspay/hyperswitch/pull/3050)) ([`7bd6e05`](https://github.com/juspay/hyperswitch/commit/7bd6e05c0c05ebae9b82a6f410e61ca4409d088b)) +- **pm_list:** Add required fields for bancontact_card for Mollie, Adyen and Stripe ([#3035](https://github.com/juspay/hyperswitch/pull/3035)) ([`792e642`](https://github.com/juspay/hyperswitch/commit/792e642ad58f90bae3ddcea5e6cbc70e948d8e28)) +- **user:** Add email apis and new enums for metadata ([#3053](https://github.com/juspay/hyperswitch/pull/3053)) ([`1c3d260`](https://github.com/juspay/hyperswitch/commit/1c3d260dc3e18fbf6cbd5122122a6c73dceb39a3)) +- Implement FRM flows ([#2968](https://github.com/juspay/hyperswitch/pull/2968)) ([`055d838`](https://github.com/juspay/hyperswitch/commit/055d8383671f6b466297c177bcc770618c7da96a)) + +### Bug Fixes + +- Remove redundant call to populate_payment_data function ([#3054](https://github.com/juspay/hyperswitch/pull/3054)) ([`53df543`](https://github.com/juspay/hyperswitch/commit/53df543b7f1407a758232025b7de0fb527be8e86)) + +### Documentation + +- **test_utils:** Update postman docs ([#3055](https://github.com/juspay/hyperswitch/pull/3055)) ([`8b7a7aa`](https://github.com/juspay/hyperswitch/commit/8b7a7aa6494ff669e1f8bcc92a5160e422d6b26e)) + +**Full Changelog:** [`v1.95.0...v1.96.0`](https://github.com/juspay/hyperswitch/compare/v1.95.0...v1.96.0) + +- - - + + ## 1.95.0 (2023-12-05) ### Features From cfafd5cd29857283d57731dda7c5a332a493f531 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:32:34 +0530 Subject: [PATCH 152/443] chore(codeowners): add codeowners for hyperswitch dashboard (#3057) Co-authored-by: Mani Chandra Dulam Co-authored-by: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> --- .github/CODEOWNERS | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3024477bac20..a911d26d8650 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -44,6 +44,60 @@ crates/router/src/core/routing.rs @juspay/hyperswitch-routing crates/router/src/core/payments/routing @juspay/hyperswitch-routing crates/router/src/core/payments/routing.rs @juspay/hyperswitch-routing +crates/api_models/src/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/api_models/src/user @juspay/hyperswitch-dashboard +crates/api_models/src/user.rs @juspay/hyperswitch-dashboard +crates/api_models/src/user_role.rs @juspay/hyperswitch-dashboard +crates/api_models/src/verify_connector.rs @juspay/hyperswitch-dashboard +crates/api_models/src/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/dashboard_metadata.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/user @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/user_role.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/query/user.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/user @juspay/hyperswitch-dashboard +crates/diesel_models/src/user.rs @juspay/hyperswitch-dashboard +crates/diesel_models/src/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/consts/user.rs @juspay/hyperswitch-dashboard +crates/router/src/consts/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/core/connector_onboarding @juspay/hyperswitch-dashboard +crates/router/src/core/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/core/errors/user.rs @juspay/hyperswitch-dashboard +crates/router/src/core/errors/user @juspay/hyperswitch-dashboard +crates/router/src/core/user @juspay/hyperswitch-dashboard +crates/router/src/core/user.rs @juspay/hyperswitch-dashboard +crates/router/src/core/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/core/verify_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/db/dashboard_metadata.rs @juspay/hyperswitch-dashboard +crates/router/src/db/user @juspay/hyperswitch-dashboard +crates/router/src/db/user.rs @juspay/hyperswitch-dashboard +crates/router/src/db/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/dummy_connector @juspay/hyperswitch-dashboard +crates/router/src/routes/dummy_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/user.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/routes/verify_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/services/authentication.rs @juspay/hyperswitch-dashboard +crates/router/src/services/authorization @juspay/hyperswitch-dashboard +crates/router/src/services/authorization.rs @juspay/hyperswitch-dashboard +crates/router/src/services/jwt.rs @juspay/hyperswitch-dashboard +crates/router/src/services/email/types.rs @juspay/hyperswitch-dashboard +crates/router/src/types/api/connector_onboarding @juspay/hyperswitch-dashboard +crates/router/src/types/api/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/types/api/verify_connector @juspay/hyperswitch-dashboard +crates/router/src/types/api/verify_connector.rs @juspay/hyperswitch-dashboard +crates/router/src/types/domain/user @juspay/hyperswitch-dashboard +crates/router/src/types/domain/user.rs @juspay/hyperswitch-dashboard +crates/router/src/types/storage/user.rs @juspay/hyperswitch-dashboard +crates/router/src/types/storage/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/types/storage/dashboard_metadata.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/connector_onboarding @juspay/hyperswitch-dashboard +crates/router/src/utils/connector_onboarding.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/user @juspay/hyperswitch-dashboard +crates/router/src/utils/user.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/user_role.rs @juspay/hyperswitch-dashboard +crates/router/src/utils/verify_connector.rs @juspay/hyperswitch-dashboard + crates/router/src/scheduler/ @juspay/hyperswitch-process-tracker Dockerfile @juspay/hyperswitch-infra From c6e2ee29d9ee4fe54e6fa6f87c2fa065a290d258 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Wed, 6 Dec 2023 11:25:00 +0530 Subject: [PATCH 153/443] feat(metrics): add drainer delay metric (#3034) --- crates/diesel_models/src/kv.rs | 3 +++ crates/drainer/src/lib.rs | 3 +++ crates/drainer/src/metrics.rs | 5 ++++- crates/drainer/src/utils.rs | 19 +++++++++++++++++++ crates/router_env/src/metrics.rs | 19 +++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index f56ef8304186..dd12a916c90f 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -31,6 +31,8 @@ impl TypedSql { request_id: String, global_id: String, ) -> crate::StorageResult> { + let pushed_at = common_utils::date_time::now_unix_timestamp(); + Ok(vec![ ( "typed_sql", @@ -40,6 +42,7 @@ impl TypedSql { ), ("global_id", global_id), ("request_id", request_id), + ("pushed_at", pushed_at.to_string()), ]) } } diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 94a29e3b0a04..7b77873a648e 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -199,6 +199,7 @@ async fn drainer( .get("request_id") .map_or(String::new(), Clone::clone); let global_id = entry.1.get("global_id").map_or(String::new(), Clone::clone); + let pushed_at = entry.1.get("pushed_at"); tracing::Span::current().record("request_id", request_id); tracing::Span::current().record("global_id", global_id); @@ -261,6 +262,7 @@ async fn drainer( value: insert_op.into(), }], ); + utils::push_drainer_delay(pushed_at, insert_op.to_string()); } kv::DBOperation::Update { updatable } => { let (_, execution_time) = common_utils::date_time::time_it(|| async { @@ -302,6 +304,7 @@ async fn drainer( value: update_op.into(), }], ); + utils::push_drainer_delay(pushed_at, update_op.to_string()); } kv::DBOperation::Delete => { // [#224]: Implement this diff --git a/crates/drainer/src/metrics.rs b/crates/drainer/src/metrics.rs index 77f3d5e7db1d..06e9119787d5 100644 --- a/crates/drainer/src/metrics.rs +++ b/crates/drainer/src/metrics.rs @@ -1,5 +1,7 @@ pub use router_env::opentelemetry::KeyValue; -use router_env::{counter_metric, global_meter, histogram_metric, metrics_context}; +use router_env::{ + counter_metric, global_meter, histogram_metric, histogram_metric_i64, metrics_context, +}; metrics_context!(CONTEXT); global_meter!(DRAINER_METER, "DRAINER"); @@ -18,3 +20,4 @@ histogram_metric!(QUERY_EXECUTION_TIME, DRAINER_METER); // Time in (ms) millisec histogram_metric!(REDIS_STREAM_READ_TIME, DRAINER_METER); // Time in (ms) milliseconds histogram_metric!(REDIS_STREAM_TRIM_TIME, DRAINER_METER); // Time in (ms) milliseconds histogram_metric!(CLEANUP_TIME, DRAINER_METER); // Time in (ms) milliseconds +histogram_metric_i64!(DRAINER_DELAY_SECONDS, DRAINER_METER); // Time in (s) seconds diff --git a/crates/drainer/src/utils.rs b/crates/drainer/src/utils.rs index 2bd9f092f12c..5d3bd241d4df 100644 --- a/crates/drainer/src/utils.rs +++ b/crates/drainer/src/utils.rs @@ -128,6 +128,25 @@ pub fn parse_stream_entries<'a>( .into_report() } +pub fn push_drainer_delay(pushed_at: Option<&String>, operation: String) { + if let Some(pushed_at) = pushed_at { + if let Ok(time) = pushed_at.parse::() { + let drained_at = common_utils::date_time::now_unix_timestamp(); + let delay = drained_at - time; + + logger::debug!(operation = operation, delay = delay); + metrics::DRAINER_DELAY_SECONDS.record( + &metrics::CONTEXT, + delay, + &[metrics::KeyValue { + key: "operation".into(), + value: operation.into(), + }], + ); + } + } +} + // Here the output is in the format (stream_index, jobs_picked), // similar to the first argument of the function pub async fn increment_stream_index( diff --git a/crates/router_env/src/metrics.rs b/crates/router_env/src/metrics.rs index 14402a7a6e91..961e0d362205 100644 --- a/crates/router_env/src/metrics.rs +++ b/crates/router_env/src/metrics.rs @@ -82,3 +82,22 @@ macro_rules! histogram_metric_u64 { > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); }; } + +/// Create a [`Histogram`][Histogram] i64 metric with the specified name and an optional description, +/// associated with the specified meter. Note that the meter must be to a valid [`Meter`][Meter]. +/// +/// [Histogram]: opentelemetry::metrics::Histogram +/// [Meter]: opentelemetry::metrics::Meter +#[macro_export] +macro_rules! histogram_metric_i64 { + ($name:ident, $meter:ident) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.i64_histogram(stringify!($name)).init()); + }; + ($name:ident, $meter:ident, $description:literal) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.i64_histogram($description).init()); + }; +} From a2405e56fbd84936a1afa6aa9f8f7e815267fbec Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Wed, 6 Dec 2023 11:26:25 +0530 Subject: [PATCH 154/443] fix: throw bad request while pushing duplicate data to redis (#3016) --- crates/router/src/core/errors/utils.rs | 22 +++++++++++++++++++ crates/router/src/db/refund.rs | 6 ++--- crates/router/src/db/reverse_lookup.rs | 3 ++- crates/router/src/utils/db_utils.rs | 8 +++++-- crates/storage_impl/src/errors.rs | 20 +++++++++++++++++ crates/storage_impl/src/lookup.rs | 3 ++- .../src/payments/payment_attempt.rs | 3 ++- .../src/payments/payment_intent.rs | 5 +++-- crates/storage_impl/src/utils.rs | 5 +++-- 9 files changed, 63 insertions(+), 12 deletions(-) diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index b62abd0e336e..f00948b887e1 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -480,3 +480,25 @@ impl ConnectorErrorExt for error_stack::Result } } } + +pub trait RedisErrorExt { + #[track_caller] + fn to_redis_failed_response(self, key: &str) -> error_stack::Report; +} + +impl RedisErrorExt for error_stack::Report { + fn to_redis_failed_response(self, key: &str) -> error_stack::Report { + match self.current_context() { + errors::RedisError::NotFound => self.change_context( + errors::StorageError::ValueNotFound(format!("Data does not exist for key {key}",)), + ), + errors::RedisError::SetNxFailed => { + self.change_context(errors::StorageError::DuplicateValue { + entity: "redis", + key: Some(key.to_string()), + }) + } + _ => self.change_context(errors::StorageError::KVError), + } + } +} diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 1ab5a8360812..accb5e8f3f94 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -275,7 +275,7 @@ mod storage { use super::RefundInterface; use crate::{ connection, - core::errors::{self, CustomResult}, + core::errors::{self, utils::RedisErrorExt, CustomResult}, db::reverse_lookup::ReverseLookupInterface, services::Store, types::storage::{self as storage_types, enums, kv}, @@ -437,7 +437,7 @@ mod storage { &key, ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hsetnx() { Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { @@ -544,7 +544,7 @@ mod storage { &key, ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hset() .change_context(errors::StorageError::KVError)?; diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index 445e171fa277..344077f3ec0f 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -69,6 +69,7 @@ mod storage { use super::{ReverseLookupInterface, Store}; use crate::{ connection, + core::errors::utils::RedisErrorExt, errors::{self, CustomResult}, types::storage::{ enums, kv, @@ -109,7 +110,7 @@ mod storage { format!("reverse_lookup_{}", &created_rev_lookup.lookup_id), ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&created_rev_lookup.lookup_id))? .try_into_setnx() { Ok(SetnxReply::KeySet) => Ok(created_rev_lookup), diff --git a/crates/router/src/utils/db_utils.rs b/crates/router/src/utils/db_utils.rs index febc226c0202..219b6f9777f9 100644 --- a/crates/router/src/utils/db_utils.rs +++ b/crates/router/src/utils/db_utils.rs @@ -1,4 +1,7 @@ -use crate::{core::errors, routes::metrics}; +use crate::{ + core::errors::{self, utils::RedisErrorExt}, + routes::metrics, +}; /// Generates hscan field pattern. Suppose the field is pa_1234_ref_1211 it will generate /// pa_1234_ref_* @@ -28,7 +31,8 @@ where metrics::KV_MISS.add(&metrics::CONTEXT, 1, &[]); database_call_closure().await } - _ => Err(redis_error.change_context(errors::StorageError::KVError)), + // Keeping the key empty here since the error would never go here. + _ => Err(redis_error.to_redis_failed_response("")), }, } } diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 105a93d4beae..f0cbebf78c55 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -150,6 +150,26 @@ impl StorageError { } } +pub trait RedisErrorExt { + #[track_caller] + fn to_redis_failed_response(self, key: &str) -> error_stack::Report; +} + +impl RedisErrorExt for error_stack::Report { + fn to_redis_failed_response(self, key: &str) -> error_stack::Report { + match self.current_context() { + RedisError::NotFound => self.change_context(DataStorageError::ValueNotFound(format!( + "Data does not exist for key {key}", + ))), + RedisError::SetNxFailed => self.change_context(DataStorageError::DuplicateValue { + entity: "redis", + key: Some(key.to_string()), + }), + _ => self.change_context(DataStorageError::KVError), + } + } +} + impl_error_type!(EncryptionError, "Encryption error"); #[derive(Debug, thiserror::Error)] diff --git a/crates/storage_impl/src/lookup.rs b/crates/storage_impl/src/lookup.rs index bd045fedd379..c96e24515772 100644 --- a/crates/storage_impl/src/lookup.rs +++ b/crates/storage_impl/src/lookup.rs @@ -11,6 +11,7 @@ use redis_interface::SetnxReply; use crate::{ diesel_error_to_data_error, + errors::RedisErrorExt, redis::kv_store::{kv_wrapper, KvOperation}, utils::{self, try_redis_get_else_try_database_get}, DatabaseStore, KVRouterStore, RouterStore, @@ -97,7 +98,7 @@ impl ReverseLookupInterface for KVRouterStore { format!("reverse_lookup_{}", &created_rev_lookup.lookup_id), ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&created_rev_lookup.lookup_id))? .try_into_setnx() { Ok(SetnxReply::KeySet) => Ok(created_rev_lookup), diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index b524ff1aaa71..425cdd216fec 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -29,6 +29,7 @@ use router_env::{instrument, tracing}; use crate::{ diesel_error_to_data_error, + errors::RedisErrorExt, lookup::ReverseLookupInterface, redis::kv_store::{kv_wrapper, KvOperation}, utils::{pg_connection_read, pg_connection_write, try_redis_get_else_try_database_get}, @@ -423,7 +424,7 @@ impl PaymentAttemptInterface for KVRouterStore { &key, ) .await - .change_context(errors::StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hsetnx() { Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 3e695947b8bf..61229ca890c3 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -38,6 +38,7 @@ use router_env::{instrument, tracing}; use crate::connection; use crate::{ diesel_error_to_data_error, + errors::RedisErrorExt, redis::kv_store::{kv_wrapper, KvOperation}, utils::{self, pg_connection_read, pg_connection_write}, DataModelExt, DatabaseStore, KVRouterStore, @@ -117,7 +118,7 @@ impl PaymentIntentInterface for KVRouterStore { &key, ) .await - .change_context(StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hsetnx() { Ok(HsetnxReply::KeyNotSet) => Err(StorageError::DuplicateValue { @@ -178,7 +179,7 @@ impl PaymentIntentInterface for KVRouterStore { &key, ) .await - .change_context(StorageError::KVError)? + .map_err(|err| err.to_redis_failed_response(&key))? .try_into_hset() .change_context(StorageError::KVError)?; diff --git a/crates/storage_impl/src/utils.rs b/crates/storage_impl/src/utils.rs index 6d6e1cd5402b..6d69f02593fd 100644 --- a/crates/storage_impl/src/utils.rs +++ b/crates/storage_impl/src/utils.rs @@ -3,7 +3,7 @@ use data_models::errors::StorageError; use diesel::PgConnection; use error_stack::{IntoReport, ResultExt}; -use crate::{metrics, DatabaseStore}; +use crate::{errors::RedisErrorExt, metrics, DatabaseStore}; pub async fn pg_connection_read( store: &T, @@ -64,7 +64,8 @@ where metrics::KV_MISS.add(&metrics::CONTEXT, 1, &[]); database_call_closure().await } - _ => Err(redis_error.change_context(StorageError::KVError)), + // Keeping the key empty here since the error would never go here. + _ => Err(redis_error.to_redis_failed_response("")), }, } } From 84decd8126d306a5e1cf22b36e1378a73dc963f5 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:47:40 +0530 Subject: [PATCH 155/443] fix(config): parse kafka brokers from env variable as sequence (#3066) --- crates/router/src/configs/settings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 6cbffc186d23..7b469a2165f4 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -785,6 +785,7 @@ impl Settings { .list_separator(",") .with_list_parse_key("log.telemetry.route_to_trace") .with_list_parse_key("redis.cluster_urls") + .with_list_parse_key("events.kafka.brokers") .with_list_parse_key("connectors.supported.wallets") .with_list_parse_key("connector_request_reference_id_config.merchant_ids_send_payment_id_as_connector_request_id"), From 47c038300adad1c02e4c77d529c7cc2457cf3b91 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:26:38 +0530 Subject: [PATCH 156/443] feat(connector): [BANKOFAMERICA] Implement Apple Pay (#3061) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/configs/defaults.rs | 89 ++++++++++++++++++- .../connector/bankofamerica/transformers.rs | 82 ++++++++++++++++- .../src/connector/checkout/transformers.rs | 24 +---- .../src/connector/stripe/transformers.rs | 26 ++---- crates/router/src/connector/utils.rs | 29 +++++- 5 files changed, 205 insertions(+), 45 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 1394c33b5505..744d7883e950 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4454,6 +4454,93 @@ impl Default for super::settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::new(), } + ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } ) ]), }, @@ -4556,7 +4643,7 @@ impl Default for super::settings::RequiredFields { ), common: HashMap::new(), } - ), + ) ]), }, ), diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index e31a69669c6d..bbec9022835c 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self, AddressDetailsData, CardData, CardIssuer, PaymentsAuthorizeRequestData, - PaymentsSyncRequestData, RouterData, + self, AddressDetailsData, ApplePayDecrypt, CardData, CardIssuer, + PaymentsAuthorizeRequestData, PaymentsSyncRequestData, RouterData, }, consts, core::errors, @@ -16,6 +16,7 @@ use crate::{ api::{self, enums as api_enums}, storage::enums, transformers::ForeignFrom, + ApplePayPredecryptData, }, }; @@ -110,11 +111,18 @@ pub struct GooglePayPaymentInformation { fluid_data: FluidData, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayPaymentInformation { + tokenized_card: TokenizedCard, +} + #[derive(Debug, Serialize)] #[serde(untagged)] pub enum PaymentInformation { Cards(CardPaymentInformation), GooglePay(GooglePayPaymentInformation), + ApplePay(ApplePayPaymentInformation), } #[derive(Debug, Serialize)] @@ -128,6 +136,16 @@ pub struct Card { card_type: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizedCard { + number: Secret, + expiration_month: Secret, + expiration_year: Secret, + cryptogram: Secret, + transaction_type: TransactionType, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FluidData { @@ -215,6 +233,12 @@ impl From for String { } } +#[derive(Debug, Serialize)] +pub enum TransactionType { + #[serde(rename = "1")] + ApplePay, +} + impl From<( &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, @@ -320,6 +344,48 @@ impl } } +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, apple_pay_data): ( + &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + let processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::ApplePay))); + let client_reference_information = ClientReferenceInformation::from(item); + + let expiration_month = apple_pay_data.get_expiry_month()?; + let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; + + let payment_information = PaymentInformation::ApplePay(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: apple_pay_data.application_primary_account_number, + cryptogram: apple_pay_data.payment_data.online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + impl TryFrom<( &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, @@ -368,6 +434,17 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> match item.router_data.request.payment_method_data.clone() { payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::ApplePay(_) => { + let payment_method_token = item.router_data.get_payment_method_token()?; + match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + } + } payments::WalletData::GooglePay(google_pay_data) => { Self::try_from((item, google_pay_data)) } @@ -378,7 +455,6 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> | payments::WalletData::KakaoPayRedirect(_) | payments::WalletData::GoPayRedirect(_) | payments::WalletData::GcashRedirect(_) - | payments::WalletData::ApplePay(_) | payments::WalletData::ApplePayRedirect(_) | payments::WalletData::ApplePayThirdPartySdk(_) | payments::WalletData::DanaRedirect {} diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index ebe02f30d5ff..37c038c22afe 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1,12 +1,12 @@ use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt}; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, PeekInterface, Secret}; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; use crate::{ - connector::utils::{self, PaymentsCaptureRequestData, RouterData, WalletData}, + connector::utils::{self, ApplePayDecrypt, PaymentsCaptureRequestData, RouterData, WalletData}, consts, core::errors, services, @@ -304,24 +304,8 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme })) } types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { - let expiry_year_4_digit = Secret::new(format!( - "20{}", - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(0..2) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - )); - let exp_month = Secret::new( - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(2..4) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - .to_owned(), - ); + let exp_month = decrypt_data.get_expiry_month()?; + let expiry_year_4_digit = decrypt_data.get_four_digit_expiry_year()?; Ok(PaymentSource::ApplePayPredecrypt(Box::new( ApplePayPredecrypt { token: decrypt_data.application_primary_account_number, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 182479604539..fad029c1c9db 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -8,7 +8,7 @@ use common_utils::{ }; use data_models::mandates::AcceptanceType; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, ExposeOptionInterface, PeekInterface, Secret}; +use masking::{ExposeInterface, ExposeOptionInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -16,7 +16,9 @@ use uuid::Uuid; use crate::{ collect_missing_value_keys, - connector::utils::{self as connector_util, ApplePay, PaymentsPreProcessingData, RouterData}, + connector::utils::{ + self as connector_util, ApplePay, ApplePayDecrypt, PaymentsPreProcessingData, RouterData, + }, core::errors, services, types::{ @@ -1473,24 +1475,8 @@ impl TryFrom<(&payments::WalletData, Option)> if let Some(types::PaymentMethodToken::ApplePayDecrypt(decrypt_data)) = payment_method_token { - let expiry_year_4_digit = Secret::new(format!( - "20{}", - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(0..2) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - )); - let exp_month = Secret::new( - decrypt_data - .clone() - .application_expiration_date - .peek() - .get(2..4) - .ok_or(errors::ConnectorError::RequestEncodingFailed)? - .to_owned(), - ); + let expiry_year_4_digit = decrypt_data.get_four_digit_expiry_year()?; + let exp_month = decrypt_data.get_expiry_month()?; Some(Self::Wallet(StripeWallet::ApplePayPredecryptToken( Box::new(StripeApplePayPredecrypt { diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 2580dcd3fc22..3990fc9c7e47 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -28,7 +28,7 @@ use crate::{ pii::PeekInterface, types::{ self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, - PaymentsCancelData, ResponseId, + ApplePayPredecryptData, PaymentsCancelData, ResponseId, }, utils::{OptionExt, ValueExt}, }; @@ -855,6 +855,33 @@ impl ApplePay for payments::ApplePayWalletData { } } +pub trait ApplePayDecrypt { + fn get_expiry_month(&self) -> Result, Error>; + fn get_four_digit_expiry_year(&self) -> Result, Error>; +} + +impl ApplePayDecrypt for Box { + fn get_four_digit_expiry_year(&self) -> Result, Error> { + Ok(Secret::new(format!( + "20{}", + self.application_expiration_date + .peek() + .get(0..2) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + ))) + } + + fn get_expiry_month(&self) -> Result, Error> { + Ok(Secret::new( + self.application_expiration_date + .peek() + .get(2..4) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_owned(), + )) + } +} + pub trait CryptoData { fn get_pay_currency(&self) -> Result; } From 8a995cefdf6806645383710c6f39d963da232e94 Mon Sep 17 00:00:00 2001 From: Brian Silah <71752651+unpervertedkid@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:59:39 +0300 Subject: [PATCH 157/443] feat(Braintree): Sync with Hyperswitch Reference (#3037) Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- crates/router/src/connector/braintree/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index 44daef94e8a6..f4bd62add3b9 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -228,17 +228,16 @@ impl types::PaymentsResponseData, >, ) -> Result { + let id = item.response.transaction.id.clone(); Ok(Self { status: enums::AttemptStatus::from(item.response.transaction.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transaction.id, - ), + resource_id: types::ResponseId::ConnectorTransactionId(id.clone()), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(id), incremental_authorization_allowed: None, }), ..item.data From 6eec06b1d6ee9a00b374905e0ab9e425d0e41095 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Wed, 6 Dec 2023 17:59:34 +0530 Subject: [PATCH 158/443] fix: return url none on complete authorize (#3067) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/core/payments/operations/payment_approve.rs | 6 +++++- .../payments/operations/payment_complete_authorize.rs | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index fee0326df03c..37a3e1a14123 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -179,7 +179,11 @@ impl payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); + payment_intent.return_url = request + .return_url + .as_ref() + .map(|a| a.to_string()) + .or(payment_intent.return_url); payment_intent.allowed_payment_method_types = request .get_allowed_payment_method_types_as_value() diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 17b71172a349..48b503b96b0d 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -131,7 +131,9 @@ impl payment_attempt.browser_info = browser_info; payment_attempt.payment_method_type = payment_method_type.or(payment_attempt.payment_method_type); - payment_attempt.payment_experience = request.payment_experience; + payment_attempt.payment_experience = request + .payment_experience + .or(payment_attempt.payment_experience); currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); @@ -173,7 +175,11 @@ impl payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string()); + payment_intent.return_url = request + .return_url + .as_ref() + .map(|a| a.to_string()) + .or(payment_intent.return_url); payment_intent.allowed_payment_method_types = request .get_allowed_payment_method_types_as_value() From 294b04bcdd44df17d373a74dcef1e47a0949f284 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:10:13 +0000 Subject: [PATCH 159/443] chore(version): v1.97.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e87adeea9357..6bfcc08d08ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.97.0 (2023-12-06) + +### Features + +- **Braintree:** Sync with Hyperswitch Reference ([#3037](https://github.com/juspay/hyperswitch/pull/3037)) ([`8a995ce`](https://github.com/juspay/hyperswitch/commit/8a995cefdf6806645383710c6f39d963da232e94)) +- **connector:** [BANKOFAMERICA] Implement Apple Pay ([#3061](https://github.com/juspay/hyperswitch/pull/3061)) ([`47c0383`](https://github.com/juspay/hyperswitch/commit/47c038300adad1c02e4c77d529c7cc2457cf3b91)) +- **metrics:** Add drainer delay metric ([#3034](https://github.com/juspay/hyperswitch/pull/3034)) ([`c6e2ee2`](https://github.com/juspay/hyperswitch/commit/c6e2ee29d9ee4fe54e6fa6f87c2fa065a290d258)) + +### Bug Fixes + +- **config:** Parse kafka brokers from env variable as sequence ([#3066](https://github.com/juspay/hyperswitch/pull/3066)) ([`84decd8`](https://github.com/juspay/hyperswitch/commit/84decd8126d306a5e1cf22b36e1378a73dc963f5)) +- Throw bad request while pushing duplicate data to redis ([#3016](https://github.com/juspay/hyperswitch/pull/3016)) ([`a2405e5`](https://github.com/juspay/hyperswitch/commit/a2405e56fbd84936a1afa6aa9f8f7e815267fbec)) +- Return url none on complete authorize ([#3067](https://github.com/juspay/hyperswitch/pull/3067)) ([`6eec06b`](https://github.com/juspay/hyperswitch/commit/6eec06b1d6ee9a00b374905e0ab9e425d0e41095)) + +### Miscellaneous Tasks + +- **codeowners:** Add codeowners for hyperswitch dashboard ([#3057](https://github.com/juspay/hyperswitch/pull/3057)) ([`cfafd5c`](https://github.com/juspay/hyperswitch/commit/cfafd5cd29857283d57731dda7c5a332a493f531)) + +**Full Changelog:** [`v1.96.0...v1.97.0`](https://github.com/juspay/hyperswitch/compare/v1.96.0...v1.97.0) + +- - - + + ## 1.96.0 (2023-12-05) ### Features From 9c1c44a706750b14857e9180f5161b61ed89a2ad Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:48:41 +0530 Subject: [PATCH 160/443] feat(pm_auth): pm_auth service migration (#3047) Co-authored-by: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sarthak Soni --- .github/workflows/CI-pr.yml | 20 + Cargo.lock | 24 + config/config.example.toml | 6 +- config/development.toml | 4 + config/docker_compose.toml | 4 + crates/api_models/Cargo.toml | 2 + crates/api_models/src/enums.rs | 25 + crates/api_models/src/lib.rs | 1 + crates/api_models/src/pm_auth.rs | 57 ++ crates/pm_auth/Cargo.toml | 27 + crates/pm_auth/README.md | 3 + crates/pm_auth/src/connector.rs | 3 + crates/pm_auth/src/connector/plaid.rs | 353 +++++++++ .../src/connector/plaid/transformers.rs | 294 +++++++ crates/pm_auth/src/consts.rs | 5 + crates/pm_auth/src/core.rs | 1 + crates/pm_auth/src/core/errors.rs | 27 + crates/pm_auth/src/lib.rs | 4 + crates/pm_auth/src/types.rs | 152 ++++ crates/pm_auth/src/types/api.rs | 167 ++++ crates/pm_auth/src/types/api/auth_service.rs | 40 + crates/router/Cargo.toml | 1 + crates/router/src/configs/settings.rs | 7 + crates/router/src/core.rs | 1 + crates/router/src/core/admin.rs | 153 +++- crates/router/src/core/payment_methods.rs | 17 +- .../router/src/core/payment_methods/cards.rs | 105 ++- crates/router/src/core/pm_auth.rs | 729 ++++++++++++++++++ crates/router/src/core/pm_auth/helpers.rs | 33 + .../router/src/core/pm_auth/transformers.rs | 18 + crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/pm_auth.rs | 73 ++ crates/router/src/services.rs | 1 + crates/router/src/services/authentication.rs | 12 + crates/router/src/services/pm_auth.rs | 95 +++ crates/router/src/types.rs | 2 + crates/router/src/types/pm_auth.rs | 38 + crates/router_env/src/logger/types.rs | 4 + 40 files changed, 2492 insertions(+), 25 deletions(-) create mode 100644 crates/api_models/src/pm_auth.rs create mode 100644 crates/pm_auth/Cargo.toml create mode 100644 crates/pm_auth/README.md create mode 100644 crates/pm_auth/src/connector.rs create mode 100644 crates/pm_auth/src/connector/plaid.rs create mode 100644 crates/pm_auth/src/connector/plaid/transformers.rs create mode 100644 crates/pm_auth/src/consts.rs create mode 100644 crates/pm_auth/src/core.rs create mode 100644 crates/pm_auth/src/core/errors.rs create mode 100644 crates/pm_auth/src/lib.rs create mode 100644 crates/pm_auth/src/types.rs create mode 100644 crates/pm_auth/src/types/api.rs create mode 100644 crates/pm_auth/src/types/api/auth_service.rs create mode 100644 crates/router/src/core/pm_auth.rs create mode 100644 crates/router/src/core/pm_auth/helpers.rs create mode 100644 crates/router/src/core/pm_auth/transformers.rs create mode 100644 crates/router/src/routes/pm_auth.rs create mode 100644 crates/router/src/services/pm_auth.rs create mode 100644 crates/router/src/types/pm_auth.rs diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index ecb13f3c1a85..79cb352acbb8 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -203,6 +203,11 @@ jobs: else echo "test_utils_changes_exist=true" >> $GITHUB_ENV fi + if git diff --submodule=diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/pm_auth/; then + echo "pm_auth_changes_exist=false" >> $GITHUB_ENV + else + echo "pm_auth_changes_exist=true" >> $GITHUB_ENV + fi - name: Cargo hack api_models if: env.api_models_changes_exist == 'true' @@ -249,6 +254,11 @@ jobs: shell: bash run: cargo hack check --each-feature --no-dev-deps -p redis_interface + - name: Cargo hack pm_auth + if: env.pm_auth_changes_exist == 'true' + shell: bash + run: cargo hack check --each-feature --no-dev-deps -p pm_auth + - name: Cargo hack router if: env.router_changes_exist == 'true' shell: bash @@ -456,6 +466,11 @@ jobs: else echo "test_utils_changes_exist=true" >> $GITHUB_ENV fi + if git diff --submodule=diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/pm_auth/; then + echo "pm_auth_changes_exist=false" >> $GITHUB_ENV + else + echo "pm_auth_changes_exist=true" >> $GITHUB_ENV + fi - name: Cargo hack api_models if: env.api_models_changes_exist == 'true' @@ -502,6 +517,11 @@ jobs: shell: bash run: cargo hack check --each-feature --no-dev-deps -p redis_interface + - name: Cargo hack pm_auth + if: env.pm_auth_changes_exist == 'true' + shell: bash + run: cargo hack check --each-feature --no-dev-deps -p pm_auth + - name: Cargo hack router if: env.router_changes_exist == 'true' shell: bash diff --git a/Cargo.lock b/Cargo.lock index d2e8d9dd5df9..307a5ca2398d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,8 @@ dependencies = [ "common_utils", "error-stack", "euclid", + "frunk", + "frunk_core", "masking", "mime", "reqwest", @@ -4436,6 +4438,27 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pm_auth" +version = "0.1.0" +dependencies = [ + "api_models", + "async-trait", + "bytes 1.5.0", + "common_enums", + "common_utils", + "error-stack", + "http", + "masking", + "mime", + "router_derive", + "router_env", + "serde", + "serde_json", + "strum 0.24.1", + "thiserror", +] + [[package]] name = "png" version = "0.16.8" @@ -5110,6 +5133,7 @@ dependencies = [ "num_cpus", "once_cell", "openssl", + "pm_auth", "qrcode", "rand 0.8.5", "rand_chacha 0.3.1", diff --git a/config/config.example.toml b/config/config.example.toml index 1897c9355812..7a50c23f484d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -122,7 +122,7 @@ kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) cipher # like card details [locker] host = "" # Locker host -host_rs = "" # Rust Locker host +host_rs = "" # Rust Locker host mock_locker = true # Emulate a locker locally using Postgres basilisk_host = "" # Basilisk host locker_signing_key_id = "1" # Key_id to sign basilisk hs locker @@ -461,6 +461,10 @@ apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" + # Analytics configuration. [analytics] source = "sqlx" # The Analytics source/strategy to be used diff --git a/config/development.toml b/config/development.toml index 4ee33795676c..15acfdee9b74 100644 --- a/config/development.toml +++ b/config/development.toml @@ -470,6 +470,10 @@ apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" + [lock_settings] redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 55fc62329d4c..5eec8d733d6a 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -330,6 +330,10 @@ payout_connector_list = "wise" [multiple_api_version_supported_connectors] supported_connectors = "braintree" +[payment_method_auth] +redis_expiry = 900 +pm_auth_key = "Some_pm_auth_key" + [lock_settings] redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 116aad25d5c8..afba129b601e 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -30,6 +30,8 @@ strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } +frunk = "0.4.1" +frunk_core = "0.4.1" # First party crates cards = { version = "0.1.0", path = "../cards" } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 17787929a463..215860540555 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + pub use common_enums::*; use utoipa::ToSchema; @@ -500,3 +502,26 @@ pub enum LockerChoice { Basilisk, Tartarus, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, + ToSchema, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PmAuthConnectors { + Plaid, +} + +pub fn convert_pm_auth_connector(connector_name: &str) -> Option { + PmAuthConnectors::from_str(connector_name).ok() +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index ce3c11d9c2f3..935944cf74c2 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -23,6 +23,7 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +pub mod pm_auth; pub mod refunds; pub mod routing; pub mod surcharge_decision_configs; diff --git a/crates/api_models/src/pm_auth.rs b/crates/api_models/src/pm_auth.rs new file mode 100644 index 000000000000..7044bd8d3352 --- /dev/null +++ b/crates/api_models/src/pm_auth.rs @@ -0,0 +1,57 @@ +use common_enums::{PaymentMethod, PaymentMethodType}; +use common_utils::{ + events::{ApiEventMetric, ApiEventsType}, + impl_misc_api_event_type, +}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub struct LinkTokenCreateRequest { + pub language: Option, // optional language field to be passed + pub client_secret: Option, // client secret to be passed in req body + pub payment_id: String, // payment_id to be passed in req body for redis pm_auth connector name fetch + pub payment_method: PaymentMethod, // payment_method to be used for filtering pm_auth connector + pub payment_method_type: PaymentMethodType, // payment_method_type to be used for filtering pm_auth connector +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct LinkTokenCreateResponse { + pub link_token: String, // link_token received in response + pub connector: String, // pm_auth connector name in response +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] + +pub struct ExchangeTokenCreateRequest { + pub public_token: String, + pub client_secret: Option, + pub payment_id: String, + pub payment_method: PaymentMethod, + pub payment_method_type: PaymentMethodType, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ExchangeTokenCreateResponse { + pub access_token: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodAuthConfig { + pub enabled_payment_methods: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodAuthConnectorChoice { + pub payment_method: PaymentMethod, + pub payment_method_type: PaymentMethodType, + pub connector_name: String, + pub mca_id: String, +} + +impl_misc_api_event_type!( + LinkTokenCreateRequest, + LinkTokenCreateResponse, + ExchangeTokenCreateRequest, + ExchangeTokenCreateResponse +); diff --git a/crates/pm_auth/Cargo.toml b/crates/pm_auth/Cargo.toml new file mode 100644 index 000000000000..a9aebc5b540a --- /dev/null +++ b/crates/pm_auth/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pm_auth" +description = "Open banking services" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +readme = "README.md" + +[dependencies] +# First party crates +api_models = { version = "0.1.0", path = "../api_models" } +common_enums = { version = "0.1.0", path = "../common_enums" } +common_utils = { version = "0.1.0", path = "../common_utils" } +masking = { version = "0.1.0", path = "../masking" } +router_derive = { version = "0.1.0", path = "../router_derive" } +router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } + +# Third party crates +async-trait = "0.1.66" +bytes = "1.4.0" +error-stack = "0.3.1" +http = "0.2.9" +mime = "0.3.17" +serde = "1.0.159" +serde_json = "1.0.91" +strum = { version = "0.24.1", features = ["derive"] } +thiserror = "1.0.43" diff --git a/crates/pm_auth/README.md b/crates/pm_auth/README.md new file mode 100644 index 000000000000..c630a7fe6761 --- /dev/null +++ b/crates/pm_auth/README.md @@ -0,0 +1,3 @@ +# Payment Method Auth Services + +An open banking services for payment method auth validation diff --git a/crates/pm_auth/src/connector.rs b/crates/pm_auth/src/connector.rs new file mode 100644 index 000000000000..56aad846e248 --- /dev/null +++ b/crates/pm_auth/src/connector.rs @@ -0,0 +1,3 @@ +pub mod plaid; + +pub use self::plaid::Plaid; diff --git a/crates/pm_auth/src/connector/plaid.rs b/crates/pm_auth/src/connector/plaid.rs new file mode 100644 index 000000000000..d25aba881d2d --- /dev/null +++ b/crates/pm_auth/src/connector/plaid.rs @@ -0,0 +1,353 @@ +pub mod transformers; + +use std::fmt::Debug; + +use common_utils::{ + ext_traits::{BytesExt, Encode}, + request::{Method, Request, RequestBody, RequestBuilder}, +}; +use error_stack::ResultExt; +use masking::{Mask, Maskable}; +use transformers as plaid; + +use crate::{ + core::errors, + types::{ + self as auth_types, + api::{ + auth_service::{self, BankAccountCredentials, ExchangeToken, LinkToken}, + ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, + }, + }, +}; + +#[derive(Debug, Clone)] +pub struct Plaid; + +impl ConnectorCommonExt for Plaid +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &auth_types::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + "Content-Type".to_string(), + self.get_content_type().to_string().into(), + )]; + + let mut auth = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut auth); + Ok(header) + } +} + +impl ConnectorCommon for Plaid { + fn id(&self) -> &'static str { + "plaid" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, _connectors: &'a auth_types::PaymentMethodAuthConnectors) -> &'a str { + "https://sandbox.plaid.com" + } + + fn get_auth_header( + &self, + auth_type: &auth_types::ConnectorAuthType, + ) -> errors::CustomResult)>, errors::ConnectorError> { + let auth = plaid::PlaidAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let client_id = auth.client_id.into_masked(); + let secret = auth.secret.into_masked(); + + Ok(vec![ + ("PLAID-CLIENT-ID".to_string(), client_id), + ("PLAID-SECRET".to_string(), secret), + ]) + } + + fn build_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidErrorResponse = + res.response + .parse_struct("PlaidErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(auth_types::ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.error_message, + reason: response.display_message, + }) + } +} + +impl auth_service::AuthService for Plaid {} +impl auth_service::AuthServiceLinkToken for Plaid {} + +impl ConnectorIntegration + for Plaid +{ + fn get_headers( + &self, + req: &auth_types::LinkTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::LinkTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/link/token/create" + )) + } + + fn get_request_body( + &self, + req: &auth_types::LinkTokenRouterData, + ) -> errors::CustomResult, errors::ConnectorError> { + let req_obj = plaid::PlaidLinkTokenRequest::try_from(req)?; + let plaid_req = RequestBody::log_and_get_request_body( + &req_obj, + Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(plaid_req)) + } + + fn build_request( + &self, + req: &auth_types::LinkTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthLinkTokenType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthLinkTokenType::get_headers( + self, req, connectors, + )?) + .body(auth_types::PaymentAuthLinkTokenType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::LinkTokenRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidLinkTokenResponse = res + .response + .parse_struct("PlaidLinkTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} + +impl auth_service::AuthServiceExchangeToken for Plaid {} + +impl + ConnectorIntegration< + ExchangeToken, + auth_types::ExchangeTokenRequest, + auth_types::ExchangeTokenResponse, + > for Plaid +{ + fn get_headers( + &self, + req: &auth_types::ExchangeTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::ExchangeTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/item/public_token/exchange" + )) + } + + fn get_request_body( + &self, + req: &auth_types::ExchangeTokenRouterData, + ) -> errors::CustomResult, errors::ConnectorError> { + let req_obj = plaid::PlaidExchangeTokenRequest::try_from(req)?; + let plaid_req = RequestBody::log_and_get_request_body( + &req_obj, + Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(plaid_req)) + } + + fn build_request( + &self, + req: &auth_types::ExchangeTokenRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthExchangeTokenType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthExchangeTokenType::get_headers( + self, req, connectors, + )?) + .body(auth_types::PaymentAuthExchangeTokenType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::ExchangeTokenRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidExchangeTokenResponse = res + .response + .parse_struct("PlaidExchangeTokenResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} + +impl auth_service::AuthServiceBankAccountCredentials for Plaid {} + +impl + ConnectorIntegration< + BankAccountCredentials, + auth_types::BankAccountCredentialsRequest, + auth_types::BankAccountCredentialsResponse, + > for Plaid +{ + fn get_headers( + &self, + req: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::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: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/auth/get")) + } + + fn get_request_body( + &self, + req: &auth_types::BankDetailsRouterData, + ) -> errors::CustomResult, errors::ConnectorError> { + let req_obj = plaid::PlaidBankAccountCredentialsRequest::try_from(req)?; + let plaid_req = RequestBody::log_and_get_request_body( + &req_obj, + Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(plaid_req)) + } + + fn build_request( + &self, + req: &auth_types::BankDetailsRouterData, + connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&auth_types::PaymentAuthBankAccountDetailsType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(auth_types::PaymentAuthBankAccountDetailsType::get_headers( + self, req, connectors, + )?) + .body(auth_types::PaymentAuthBankAccountDetailsType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &auth_types::BankDetailsRouterData, + res: auth_types::Response, + ) -> errors::CustomResult { + let response: plaid::PlaidBankAccountCredentialsResponse = res + .response + .parse_struct("PlaidBankAccountCredentialsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(auth_types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: auth_types::Response, + ) -> errors::CustomResult { + self.build_error_response(res) + } +} diff --git a/crates/pm_auth/src/connector/plaid/transformers.rs b/crates/pm_auth/src/connector/plaid/transformers.rs new file mode 100644 index 000000000000..5e1ad67aead0 --- /dev/null +++ b/crates/pm_auth/src/connector/plaid/transformers.rs @@ -0,0 +1,294 @@ +use std::collections::HashMap; + +use common_enums::PaymentMethodType; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{core::errors, types}; + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidLinkTokenRequest { + client_name: String, + country_codes: Vec, + language: String, + products: Vec, + user: User, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] + +pub struct User { + pub client_user_id: String, +} + +impl TryFrom<&types::LinkTokenRouterData> for PlaidLinkTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::LinkTokenRouterData) -> Result { + Ok(Self { + client_name: item.request.client_name.clone(), + country_codes: item.request.country_codes.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "country_codes", + }, + )?, + language: item.request.language.clone().unwrap_or("en".to_string()), + products: vec!["auth".to_string()], + user: User { + client_user_id: item.request.user_info.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "country_codes", + }, + )?, + }, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidLinkTokenResponse { + link_token: String, +} + +impl + TryFrom> + for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::LinkTokenResponse { + link_token: item.response.link_token, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidExchangeTokenRequest { + public_token: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidExchangeTokenResponse { + pub access_token: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlaidExchangeTokenResponse, + T, + types::ExchangeTokenResponse, + >, + ) -> Result { + Ok(Self { + response: Ok(types::ExchangeTokenResponse { + access_token: item.response.access_token, + }), + ..item.data + }) + } +} + +impl TryFrom<&types::ExchangeTokenRouterData> for PlaidExchangeTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::ExchangeTokenRouterData) -> Result { + Ok(Self { + public_token: item.request.public_token.clone(), + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidBankAccountCredentialsRequest { + access_token: String, + options: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidBankAccountCredentialsResponse { + pub accounts: Vec, + pub numbers: PlaidBankAccountCredentialsNumbers, + // pub item: PlaidBankAccountCredentialsItem, + pub request_id: String, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct BankAccountCredentialsOptions { + account_ids: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] + +pub struct PlaidBankAccountCredentialsAccounts { + pub account_id: String, + pub name: String, + pub subtype: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsBalances { + pub available: Option, + pub current: Option, + pub limit: Option, + pub iso_currency_code: Option, + pub unofficial_currency_code: Option, + pub last_updated_datetime: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsNumbers { + pub ach: Vec, + pub eft: Vec, + pub international: Vec, + pub bacs: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsItem { + pub item_id: String, + pub institution_id: Option, + pub webhook: Option, + pub error: Option, +} +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsACH { + pub account_id: String, + pub account: String, + pub routing: String, + pub wire_routing: Option, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsEFT { + pub account_id: String, + pub account: String, + pub institution: String, + pub branch: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsInternational { + pub account_id: String, + pub iban: String, + pub bic: String, +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub struct PlaidBankAccountCredentialsBacs { + pub account_id: String, + pub account: String, + pub sort_code: String, +} + +impl TryFrom<&types::BankDetailsRouterData> for PlaidBankAccountCredentialsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::BankDetailsRouterData) -> Result { + Ok(Self { + access_token: item.request.access_token.clone(), + options: item.request.optional_ids.as_ref().map(|bank_account_ids| { + BankAccountCredentialsOptions { + account_ids: bank_account_ids.ids.clone(), + } + }), + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + PlaidBankAccountCredentialsResponse, + T, + types::BankAccountCredentialsResponse, + >, + > for types::PaymentAuthRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlaidBankAccountCredentialsResponse, + T, + types::BankAccountCredentialsResponse, + >, + ) -> Result { + let (account_numbers, accounts_info) = (item.response.numbers, item.response.accounts); + let mut bank_account_vec = Vec::new(); + let mut id_to_suptype = HashMap::new(); + + accounts_info.into_iter().for_each(|acc| { + id_to_suptype.insert(acc.account_id, (acc.subtype, acc.name)); + }); + + account_numbers.ach.into_iter().for_each(|ach| { + let (acc_type, acc_name) = + if let Some((_type, name)) = id_to_suptype.get(&ach.account_id) { + (_type.to_owned(), Some(name.clone())) + } else { + (None, None) + }; + + let bank_details_new = types::BankAccountDetails { + account_name: acc_name, + account_number: ach.account, + routing_number: ach.routing, + payment_method_type: PaymentMethodType::Ach, + account_id: ach.account_id, + account_type: acc_type, + }; + + bank_account_vec.push(bank_details_new); + }); + + Ok(Self { + response: Ok(types::BankAccountCredentialsResponse { + credentials: bank_account_vec, + }), + ..item.data + }) + } +} +pub struct PlaidAuthType { + pub client_id: Secret, + pub secret: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for PlaidAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { client_id, secret } => Ok(Self { + client_id: client_id.to_owned(), + secret: secret.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct PlaidErrorResponse { + pub display_message: Option, + pub error_code: Option, + pub error_message: String, + pub error_type: Option, +} diff --git a/crates/pm_auth/src/consts.rs b/crates/pm_auth/src/consts.rs new file mode 100644 index 000000000000..dac3485ec8fc --- /dev/null +++ b/crates/pm_auth/src/consts.rs @@ -0,0 +1,5 @@ +pub const REQUEST_TIME_OUT: u64 = 30; // will timeout after the mentioned limit +pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; // timeout error code +pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; // error message for timed out request +pub const NO_ERROR_CODE: &str = "No error code"; +pub const NO_ERROR_MESSAGE: &str = "No error message"; diff --git a/crates/pm_auth/src/core.rs b/crates/pm_auth/src/core.rs new file mode 100644 index 000000000000..629e98fbf874 --- /dev/null +++ b/crates/pm_auth/src/core.rs @@ -0,0 +1 @@ +pub mod errors; diff --git a/crates/pm_auth/src/core/errors.rs b/crates/pm_auth/src/core/errors.rs new file mode 100644 index 000000000000..31b178a6276f --- /dev/null +++ b/crates/pm_auth/src/core/errors.rs @@ -0,0 +1,27 @@ +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ConnectorError { + #[error("Failed to obtain authentication type")] + FailedToObtainAuthType, + #[error("Missing required field: {field_name}")] + MissingRequiredField { field_name: &'static str }, + #[error("Failed to execute a processing step: {0:?}")] + ProcessingStepFailed(Option), + #[error("Failed to deserialize connector response")] + ResponseDeserializationFailed, + #[error("Failed to encode connector request")] + RequestEncodingFailed, +} + +pub type CustomResult = error_stack::Result; + +#[derive(Debug, thiserror::Error)] +pub enum ParsingError { + #[error("Failed to parse enum: {0}")] + EnumParseFailure(&'static str), + #[error("Failed to parse struct: {0}")] + StructParseFailure(&'static str), + #[error("Failed to serialize to {0} format")] + EncodeError(&'static str), + #[error("Unknown error while parsing")] + UnknownError, +} diff --git a/crates/pm_auth/src/lib.rs b/crates/pm_auth/src/lib.rs new file mode 100644 index 000000000000..60d0e06a1e00 --- /dev/null +++ b/crates/pm_auth/src/lib.rs @@ -0,0 +1,4 @@ +pub mod connector; +pub mod consts; +pub mod core; +pub mod types; diff --git a/crates/pm_auth/src/types.rs b/crates/pm_auth/src/types.rs new file mode 100644 index 000000000000..6f5875247f1f --- /dev/null +++ b/crates/pm_auth/src/types.rs @@ -0,0 +1,152 @@ +pub mod api; + +use std::marker::PhantomData; + +use api::auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}; +use common_enums::PaymentMethodType; +use masking::Secret; +#[derive(Debug, Clone)] +pub struct PaymentAuthRouterData { + pub flow: PhantomData, + pub merchant_id: Option, + pub connector: Option, + pub request: Request, + pub response: Result, + pub connector_auth_type: ConnectorAuthType, + pub connector_http_status_code: Option, +} + +#[derive(Debug, Clone)] +pub struct LinkTokenRequest { + pub client_name: String, + pub country_codes: Option>, + pub language: Option, + pub user_info: Option, +} + +#[derive(Debug, Clone)] +pub struct LinkTokenResponse { + pub link_token: String, +} + +pub type LinkTokenRouterData = + PaymentAuthRouterData; + +#[derive(Debug, Clone)] +pub struct ExchangeTokenRequest { + pub public_token: String, +} + +#[derive(Debug, Clone)] +pub struct ExchangeTokenResponse { + pub access_token: String, +} + +impl From for api_models::pm_auth::ExchangeTokenCreateResponse { + fn from(value: ExchangeTokenResponse) -> Self { + Self { + access_token: value.access_token, + } + } +} + +pub type ExchangeTokenRouterData = + PaymentAuthRouterData; + +#[derive(Debug, Clone)] +pub struct BankAccountCredentialsRequest { + pub access_token: String, + pub optional_ids: Option, +} + +#[derive(Debug, Clone)] +pub struct BankAccountOptionalIDs { + pub ids: Vec, +} + +#[derive(Debug, Clone)] +pub struct BankAccountCredentialsResponse { + pub credentials: Vec, +} + +#[derive(Debug, Clone)] +pub struct BankAccountDetails { + pub account_name: Option, + pub account_number: String, + pub routing_number: String, + pub payment_method_type: PaymentMethodType, + pub account_id: String, + pub account_type: Option, +} + +pub type BankDetailsRouterData = PaymentAuthRouterData< + BankAccountCredentials, + BankAccountCredentialsRequest, + BankAccountCredentialsResponse, +>; + +pub type PaymentAuthLinkTokenType = + dyn self::api::ConnectorIntegration; + +pub type PaymentAuthExchangeTokenType = + dyn self::api::ConnectorIntegration; + +pub type PaymentAuthBankAccountDetailsType = dyn self::api::ConnectorIntegration< + BankAccountCredentials, + BankAccountCredentialsRequest, + BankAccountCredentialsResponse, +>; + +#[derive(Clone, Debug, strum::EnumString, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum PaymentMethodAuthConnectors { + Plaid, +} + +#[derive(Debug, Clone)] +pub struct ResponseRouterData { + pub response: R, + pub data: PaymentAuthRouterData, + pub http_code: u16, +} + +#[derive(Clone, Debug, serde::Serialize)] +pub struct ErrorResponse { + pub code: String, + pub message: String, + pub reason: Option, + pub status_code: u16, +} + +impl ErrorResponse { + fn get_not_implemented() -> Self { + Self { + code: "IR_00".to_string(), + message: "This API is under development and will be made available soon.".to_string(), + reason: None, + status_code: http::StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + } + } +} + +#[derive(Default, Debug, Clone, serde::Deserialize)] +pub enum ConnectorAuthType { + BodyKey { + client_id: Secret, + secret: Secret, + }, + #[default] + NoKey, +} + +#[derive(Clone, Debug)] +pub struct Response { + pub headers: Option, + pub response: bytes::Bytes, + pub status_code: u16, +} + +#[derive(serde::Deserialize, Clone)] +pub struct AuthServiceQueryParam { + pub client_secret: Option, +} diff --git a/crates/pm_auth/src/types/api.rs b/crates/pm_auth/src/types/api.rs new file mode 100644 index 000000000000..2416d0fee1de --- /dev/null +++ b/crates/pm_auth/src/types/api.rs @@ -0,0 +1,167 @@ +pub mod auth_service; + +use std::fmt::Debug; + +use common_utils::{ + errors::CustomResult, + request::{Request, RequestBody}, +}; +use masking::Maskable; + +use crate::{ + core::errors::ConnectorError, + types::{self as auth_types, api::auth_service::AuthService}, +}; + +#[async_trait::async_trait] +pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { + fn get_headers( + &self, + _req: &super::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult)>, ConnectorError> { + Ok(vec![]) + } + + fn get_content_type(&self) -> &'static str { + mime::APPLICATION_JSON.essence_str() + } + + fn get_url( + &self, + _req: &super::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult { + Ok(String::new()) + } + + fn get_request_body( + &self, + _req: &super::PaymentAuthRouterData, + ) -> CustomResult, ConnectorError> { + Ok(None) + } + + fn build_request( + &self, + _req: &super::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult, ConnectorError> { + Ok(None) + } + + fn handle_response( + &self, + data: &super::PaymentAuthRouterData, + _res: auth_types::Response, + ) -> CustomResult, ConnectorError> + where + T: Clone, + Req: Clone, + Resp: Clone, + { + Ok(data.clone()) + } + + fn get_error_response( + &self, + _res: auth_types::Response, + ) -> CustomResult { + Ok(auth_types::ErrorResponse::get_not_implemented()) + } + + fn get_5xx_error_response( + &self, + res: auth_types::Response, + ) -> CustomResult { + let error_message = match res.status_code { + 500 => "internal_server_error", + 501 => "not_implemented", + 502 => "bad_gateway", + 503 => "service_unavailable", + 504 => "gateway_timeout", + 505 => "http_version_not_supported", + 506 => "variant_also_negotiates", + 507 => "insufficient_storage", + 508 => "loop_detected", + 510 => "not_extended", + 511 => "network_authentication_required", + _ => "unknown_error", + }; + Ok(auth_types::ErrorResponse { + code: res.status_code.to_string(), + message: error_message.to_string(), + reason: String::from_utf8(res.response.to_vec()).ok(), + status_code: res.status_code, + }) + } +} + +pub trait ConnectorCommonExt: + ConnectorCommon + ConnectorIntegration +{ + fn build_headers( + &self, + _req: &auth_types::PaymentAuthRouterData, + _connectors: &auth_types::PaymentMethodAuthConnectors, + ) -> CustomResult)>, ConnectorError> { + Ok(Vec::new()) + } +} + +pub type BoxedConnectorIntegration<'a, T, Req, Resp> = + Box<&'a (dyn ConnectorIntegration + Send + Sync)>; + +pub trait ConnectorIntegrationAny: Send + Sync + 'static { + fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>; +} + +impl ConnectorIntegrationAny for S +where + S: ConnectorIntegration, +{ + fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { + Box::new(self) + } +} + +pub trait AuthServiceConnector: AuthService + Send + Debug {} + +impl AuthServiceConnector for T {} + +pub type BoxedPaymentAuthConnector = Box<&'static (dyn AuthServiceConnector + Sync)>; + +#[derive(Clone, Debug)] +pub struct PaymentAuthConnectorData { + pub connector: BoxedPaymentAuthConnector, + pub connector_name: super::PaymentMethodAuthConnectors, +} + +pub trait ConnectorCommon { + fn id(&self) -> &'static str; + + fn get_auth_header( + &self, + _auth_type: &auth_types::ConnectorAuthType, + ) -> CustomResult)>, ConnectorError> { + Ok(Vec::new()) + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a auth_types::PaymentMethodAuthConnectors) -> &'a str; + + fn build_error_response( + &self, + res: auth_types::Response, + ) -> CustomResult { + Ok(auth_types::ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: crate::consts::NO_ERROR_MESSAGE.to_string(), + reason: None, + }) + } +} diff --git a/crates/pm_auth/src/types/api/auth_service.rs b/crates/pm_auth/src/types/api/auth_service.rs new file mode 100644 index 000000000000..35d44970d518 --- /dev/null +++ b/crates/pm_auth/src/types/api/auth_service.rs @@ -0,0 +1,40 @@ +use crate::types::{ + BankAccountCredentialsRequest, BankAccountCredentialsResponse, ExchangeTokenRequest, + ExchangeTokenResponse, LinkTokenRequest, LinkTokenResponse, +}; + +pub trait AuthService: + super::ConnectorCommon + + AuthServiceLinkToken + + AuthServiceExchangeToken + + AuthServiceBankAccountCredentials +{ +} + +#[derive(Debug, Clone)] +pub struct LinkToken; + +pub trait AuthServiceLinkToken: + super::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct ExchangeToken; + +pub trait AuthServiceExchangeToken: + super::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct BankAccountCredentials; + +pub trait AuthServiceBankAccountCredentials: + super::ConnectorIntegration< + BankAccountCredentials, + BankAccountCredentialsRequest, + BankAccountCredentialsResponse, +> +{ +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 791f617b30df..e498658e4577 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -111,6 +111,7 @@ currency_conversion = { version = "0.1.0", path = "../currency_conversion" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } +pm_auth = { version = "0.1.0", path = "../pm_auth", package = "pm_auth" } external_services = { version = "0.1.0", path = "../external_services" } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } masking = { version = "0.1.0", path = "../masking" } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 7b469a2165f4..1c885e90cc75 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -100,6 +100,7 @@ pub struct Settings { pub required_fields: RequiredFields, pub delayed_session_response: DelayedSessionConfig, pub webhook_source_verification_call: WebhookSourceVerificationCall, + pub payment_method_auth: PaymentMethodAuth, pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig, #[cfg(feature = "payouts")] pub payouts: Payouts, @@ -154,6 +155,12 @@ pub struct ForexApi { pub redis_lock_timeout: u64, } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct PaymentMethodAuth { + pub redis_expiry: i64, + pub pm_auth_key: String, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct DefaultExchangeRates { pub base_currency: String, diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index be83de849161..0bd197ee22e9 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -24,6 +24,7 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +pub mod pm_auth; pub mod refunds; pub mod routing; pub mod surcharge_decision_config; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 5ab543d382f5..113bc7d677d2 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -10,9 +10,10 @@ use common_utils::{ ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, }; -use error_stack::{report, FutureExt, ResultExt}; +use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; +use pm_auth::connector::plaid::transformers::PlaidAuthType; use uuid::Uuid; use crate::{ @@ -762,7 +763,7 @@ pub async fn create_payment_connector( ) .await?; - let routable_connector = + let mut routable_connector = api_enums::RoutableConnectors::from_str(&req.connector_name.to_string()).ok(); let business_profile = state @@ -773,6 +774,30 @@ pub async fn create_payment_connector( id: profile_id.to_owned(), })?; + let pm_auth_connector = + api_enums::convert_pm_auth_connector(req.connector_name.to_string().as_str()); + + let is_unroutable_connector = if pm_auth_connector.is_some() { + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid connector type given".to_string(), + }) + .into_report(); + } + true + } else { + let routable_connector_option = req + .connector_name + .to_string() + .parse() + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid connector name given".to_string(), + })?; + routable_connector = Some(routable_connector_option); + false + }; + // If connector label is not passed in the request, generate one let connector_label = req .connector_label @@ -877,6 +902,20 @@ pub async fn create_payment_connector( api_enums::ConnectorStatus::Active, )?; + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if let Some(val) = req.pm_auth_config.clone() { + validate_pm_auth( + val, + &*state.clone().store, + merchant_id.clone().as_str(), + &key_store, + merchant_account, + &Some(profile_id.clone()), + ) + .await?; + } + } + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -948,7 +987,7 @@ pub async fn create_payment_connector( #[cfg(feature = "connector_choice_mca_id")] merchant_connector_id: Some(mca.merchant_connector_id.clone()), #[cfg(not(feature = "connector_choice_mca_id"))] - sub_label: req.business_sub_label, + sub_label: req.business_sub_label.clone(), }; if !default_routing_config.contains(&choice) { @@ -956,7 +995,7 @@ pub async fn create_payment_connector( routing_helpers::update_merchant_default_config( &*state.store, merchant_id, - default_routing_config, + default_routing_config.clone(), ) .await?; } @@ -965,7 +1004,7 @@ pub async fn create_payment_connector( routing_helpers::update_merchant_default_config( &*state.store, &profile_id.clone(), - default_routing_config_for_profile, + default_routing_config_for_profile.clone(), ) .await?; } @@ -980,10 +1019,92 @@ pub async fn create_payment_connector( ], ); + if !is_unroutable_connector { + if let Some(routable_connector_val) = routable_connector { + let choice = routing_types::RoutableConnectorChoice { + #[cfg(feature = "backwards_compatibility")] + choice_kind: routing_types::RoutableChoiceKind::FullStruct, + connector: routable_connector_val, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: Some(mca.merchant_connector_id.clone()), + #[cfg(not(feature = "connector_choice_mca_id"))] + sub_label: req.business_sub_label.clone(), + }; + + if !default_routing_config.contains(&choice) { + default_routing_config.push(choice.clone()); + routing_helpers::update_merchant_default_config( + &*state.clone().store, + merchant_id, + default_routing_config, + ) + .await?; + } + + if !default_routing_config_for_profile.contains(&choice) { + default_routing_config_for_profile.push(choice); + routing_helpers::update_merchant_default_config( + &*state.store, + &profile_id, + default_routing_config_for_profile, + ) + .await?; + } + } + }; + let mca_response = mca.try_into()?; Ok(service_api::ApplicationResponse::Json(mca_response)) } +async fn validate_pm_auth( + val: serde_json::Value, + db: &dyn StorageInterface, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + profile_id: &Option, +) -> RouterResponse<()> { + let config = serde_json::from_value::(val) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "invalid data received for payment method auth config".to_string(), + }) + .attach_printable("Failed to deserialize Payment Method Auth config")?; + + let all_mcas = db + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + merchant_id, + true, + key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + for conn_choice in config.enabled_payment_methods { + let pm_auth_mca = all_mcas + .clone() + .into_iter() + .find(|mca| mca.merchant_connector_id == conn_choice.mca_id) + .ok_or(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment method auth connector account not found".to_string(), + }) + .into_report()?; + + if &pm_auth_mca.profile_id != profile_id { + return Err(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment method auth profile_id differs from connector profile_id" + .to_string(), + }) + .into_report(); + } + } + + Ok(services::ApplicationResponse::StatusOk) +} + pub async fn retrieve_payment_connector( state: AppState, merchant_id: String, @@ -1066,7 +1187,7 @@ pub async fn update_payment_connector( .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - let _merchant_account = db + let merchant_account = db .find_merchant_account_by_merchant_id(merchant_id, &key_store) .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; @@ -1106,6 +1227,20 @@ pub async fn update_payment_connector( let (connector_status, disabled) = validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; + if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { + if let Some(val) = req.pm_auth_config.clone() { + validate_pm_auth( + val, + db, + merchant_id, + &key_store, + merchant_account, + &mca.profile_id, + ) + .await?; + } + } + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), @@ -1720,8 +1855,10 @@ pub(crate) fn validate_auth_and_metadata_type( signifyd::transformers::SignifydAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Plaid => Err(report!(errors::ConnectorError::InvalidConnectorName) - .attach_printable(format!("invalid connector name: {connector_name}"))), + api_enums::Connector::Plaid => { + PlaidAuthType::foreign_try_from(val)?; + Ok(()) + } } } diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index a2dbfb1480c4..14a39f1d9556 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -11,12 +11,12 @@ pub use api_models::{ pub use common_utils::request::RequestBody; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; -use error_stack::IntoReport; use crate::{ core::{ - errors::{self, RouterResult}, + errors::RouterResult, payments::helpers, + pm_auth::{self as core_pm_auth}, }, routes::AppState, types::{ @@ -172,11 +172,14 @@ impl PaymentMethodRetrieve for Oss { .map(|card| Some((card, enums::PaymentMethod::Card))) } - storage::PaymentTokenData::AuthBankDebit(_) => { - Err(errors::ApiErrorResponse::NotImplemented { - message: errors::NotImplementedMessage::Default, - }) - .into_report() + storage::PaymentTokenData::AuthBankDebit(auth_token) => { + core_pm_auth::retrieve_payment_method_from_auth_service( + state, + merchant_key_store, + auth_token, + payment_intent, + ) + .await } } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index bbcfe45a1d0c..84aef952a531 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -13,6 +13,7 @@ use api_models::{ ResponsePaymentMethodsEnabled, }, payments::BankCodeResponse, + pm_auth::PaymentMethodAuthConfig, surcharge_decision_configs as api_surcharge_decision_configs, }; use common_utils::{ @@ -29,6 +30,8 @@ use super::surcharge_decision_configs::{ perform_surcharge_decision_management_for_payment_method_list, perform_surcharge_decision_management_for_saved_cards, }; +#[cfg(not(feature = "connector_choice_mca_id"))] +use crate::core::utils::get_connector_label; use crate::{ configs::settings, core::{ @@ -1081,9 +1084,9 @@ pub async fn list_payment_methods( logger::debug!(mca_before_filtering=?filtered_mcas); let mut response: Vec = vec![]; - for mca in filtered_mcas { - let payment_methods = match mca.payment_methods_enabled { - Some(pm) => pm, + for mca in &filtered_mcas { + let payment_methods = match &mca.payment_methods_enabled { + Some(pm) => pm.clone(), None => continue, }; @@ -1094,13 +1097,15 @@ pub async fn list_payment_methods( payment_intent.as_ref(), payment_attempt.as_ref(), billing_address.as_ref(), - mca.connector_name, + mca.connector_name.clone(), pm_config_mapping, &state.conf.mandates.supported_payment_methods, ) .await?; } + let mut pmt_to_auth_connector = HashMap::new(); + if let Some((payment_attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent.as_ref()) { @@ -1204,6 +1209,84 @@ pub async fn list_payment_methods( pre_routing_results.insert(pm_type, routable_choice); } + let redis_conn = db + .get_redis_conn() + .map_err(|redis_error| logger::error!(?redis_error)) + .ok(); + + let mut val = Vec::new(); + + for (payment_method_type, routable_connector_choice) in &pre_routing_results { + #[cfg(not(feature = "connector_choice_mca_id"))] + let connector_label = get_connector_label( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + routable_connector_choice.sub_label.as_ref(), + #[cfg(feature = "connector_choice_mca_id")] + None, + routable_connector_choice.connector.to_string().as_str(), + ); + #[cfg(not(feature = "connector_choice_mca_id"))] + let matched_mca = filtered_mcas + .iter() + .find(|m| connector_label == m.connector_label); + + #[cfg(feature = "connector_choice_mca_id")] + let matched_mca = filtered_mcas.iter().find(|m| { + routable_connector_choice.merchant_connector_id.as_ref() + == Some(&m.merchant_connector_id) + }); + + if let Some(m) = matched_mca { + let pm_auth_config = m + .pm_auth_config + .as_ref() + .map(|config| { + serde_json::from_value::(config.clone()) + .into_report() + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize Payment Method Auth config") + }) + .transpose() + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + + let matched_config = match pm_auth_config { + Some(config) => { + let internal_config = config + .enabled_payment_methods + .iter() + .find(|config| config.payment_method_type == *payment_method_type) + .cloned(); + + internal_config + } + None => None, + }; + + if let Some(config) = matched_config { + pmt_to_auth_connector + .insert(*payment_method_type, config.connector_name.clone()); + val.push(config); + } + } + } + + let pm_auth_key = format!("pm_auth_{}", payment_intent.payment_id); + let redis_expiry = state.conf.payment_method_auth.redis_expiry; + + if let Some(rc) = redis_conn { + rc.serialize_and_set_key_with_expiry(pm_auth_key.as_str(), val, redis_expiry) + .await + .attach_printable("Failed to store pm auth data in redis") + .unwrap_or_else(|err| { + logger::error!(error=?err); + }) + }; + routing_info.pre_routing_results = Some(pre_routing_results); let encoded = utils::Encode::::encode_to_value(&routing_info) @@ -1461,7 +1544,9 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector + .get(payment_method_types_hm.0) + .cloned(), }) } @@ -1496,7 +1581,9 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector + .get(payment_method_types_hm.0) + .cloned(), }) } @@ -1526,7 +1613,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1559,7 +1646,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } @@ -1592,7 +1679,7 @@ pub async fn list_payment_methods( .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), surcharge_details: None, - pm_auth_connector: None, + pm_auth_connector: pmt_to_auth_connector.get(&payment_method_type).cloned(), } }) } diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs new file mode 100644 index 000000000000..821f049d8cfc --- /dev/null +++ b/crates/router/src/core/pm_auth.rs @@ -0,0 +1,729 @@ +use std::{collections::HashMap, str::FromStr}; + +use api_models::{ + enums, + payment_methods::{self, BankAccountAccessCreds}, + payments::{AddressDetails, BankDebitBilling, BankDebitData, PaymentMethodData}, +}; +use hex; +pub mod helpers; +pub mod transformers; + +use common_utils::{ + consts, + crypto::{HmacSha256, SignMessage}, + ext_traits::AsyncExt, + generate_id, +}; +use data_models::payments::PaymentIntent; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +pub use external_services::kms; +use helpers::PaymentAuthConnectorDataExt; +use masking::{ExposeInterface, PeekInterface}; +use pm_auth::{ + connector::plaid::transformers::PlaidAuthType, + types::{ + self as pm_auth_types, + api::{ + auth_service::{BankAccountCredentials, ExchangeToken, LinkToken}, + BoxedConnectorIntegration, PaymentAuthConnectorData, + }, + }, +}; + +use crate::{ + core::{ + errors::{self, ApiErrorResponse, RouterResponse, RouterResult, StorageErrorExt}, + payment_methods::cards, + payments::helpers as oss_helpers, + pm_auth::helpers::{self as pm_auth_helpers}, + }, + db::StorageInterface, + logger, + routes::AppState, + services::{ + pm_auth::{self as pm_auth_services}, + ApplicationResponse, + }, + types::{ + self, + domain::{self, types::decrypt}, + storage, + transformers::ForeignTryFrom, + }, +}; + +pub async fn create_link_token( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::LinkTokenCreateRequest, +) -> RouterResponse { + let db = &*state.store; + + let redis_conn = db + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let pm_auth_key = format!("pm_auth_{}", payload.payment_id); + + let pm_auth_configs = redis_conn + .get_and_deserialize_key::>( + pm_auth_key.as_str(), + "Vec", + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get payment method auth choices from redis")?; + + let selected_config = pm_auth_configs + .into_iter() + .find(|config| { + config.payment_method == payload.payment_method + && config.payment_method_type == payload.payment_method_type + }) + .ok_or(ApiErrorResponse::GenericNotFoundError { + message: "payment method auth connector name not found".to_string(), + }) + .into_report()?; + + let connector_name = selected_config.connector_name.as_str(); + + let connector = PaymentAuthConnectorData::get_connector_by_name(connector_name)?; + let connector_integration: BoxedConnectorIntegration< + '_, + LinkToken, + pm_auth_types::LinkTokenRequest, + pm_auth_types::LinkTokenResponse, + > = connector.connector.get_connector_integration(); + + let payment_intent = oss_helpers::verify_payment_intent_time_and_client_secret( + &*state.store, + &merchant_account, + payload.client_secret, + ) + .await?; + + let billing_country = payment_intent + .as_ref() + .async_map(|pi| async { + oss_helpers::get_address_by_id( + &*state.store, + pi.billing_address_id.clone(), + &key_store, + pi.payment_id.clone(), + merchant_account.merchant_id.clone(), + merchant_account.storage_scheme, + ) + .await + }) + .await + .transpose()? + .flatten() + .and_then(|address| address.country) + .map(|country| country.to_string()); + + let merchant_connector_account = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_account.merchant_id.as_str(), + &selected_config.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + let auth_type = helpers::get_connector_auth_type(merchant_connector_account)?; + + let router_data = pm_auth_types::LinkTokenRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_account.merchant_id), + connector: Some(connector_name.to_string()), + request: pm_auth_types::LinkTokenRequest { + client_name: "HyperSwitch".to_string(), + country_codes: Some(vec![billing_country.ok_or( + errors::ApiErrorResponse::MissingRequiredField { + field_name: "billing_country", + }, + )?]), + language: payload.language, + user_info: payment_intent.and_then(|pi| pi.customer_id), + }, + response: Ok(pm_auth_types::LinkTokenResponse { + link_token: "".to_string(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type, + }; + + let connector_resp = pm_auth_services::execute_connector_processing_step( + state.as_ref(), + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling link token creation connector api")?; + + let link_token_resp = + connector_resp + .response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let response = api_models::pm_auth::LinkTokenCreateResponse { + link_token: link_token_resp.link_token, + connector: connector.connector_name.to_string(), + }; + + Ok(ApplicationResponse::Json(response)) +} + +impl ForeignTryFrom<&types::ConnectorAuthType> for PlaidAuthType { + type Error = errors::ConnectorError; + + fn foreign_try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => { + Ok::(Self { + client_id: api_key.to_owned(), + secret: key1.to_owned(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType), + } + } +} + +pub async fn exchange_token_core( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::ExchangeTokenCreateRequest, +) -> RouterResponse<()> { + let db = &*state.store; + + let config = get_selected_config_from_redis(db, &payload).await?; + + let connector_name = config.connector_name.as_str(); + + let connector = + pm_auth_types::api::PaymentAuthConnectorData::get_connector_by_name(connector_name)?; + + let merchant_connector_account = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_account.merchant_id.as_str(), + &config.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + })?; + + let auth_type = helpers::get_connector_auth_type(merchant_connector_account.clone())?; + + let access_token = get_access_token_from_exchange_api( + &connector, + connector_name, + &payload, + &auth_type, + &state, + ) + .await?; + + let bank_account_details_resp = get_bank_account_creds( + connector, + &merchant_account, + connector_name, + &access_token, + auth_type, + &state, + None, + ) + .await?; + + Box::pin(store_bank_details_in_payment_methods( + key_store, + payload, + merchant_account, + state, + bank_account_details_resp, + (connector_name, access_token), + merchant_connector_account.merchant_connector_id, + )) + .await?; + + Ok(ApplicationResponse::StatusOk) +} + +async fn store_bank_details_in_payment_methods( + key_store: domain::MerchantKeyStore, + payload: api_models::pm_auth::ExchangeTokenCreateRequest, + merchant_account: domain::MerchantAccount, + state: AppState, + bank_account_details_resp: pm_auth_types::BankAccountCredentialsResponse, + connector_details: (&str, String), + mca_id: String, +) -> RouterResult<()> { + let key = key_store.key.get_inner().peek(); + let db = &*state.clone().store; + let (connector_name, access_token) = connector_details; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &payload.payment_id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(ApiErrorResponse::PaymentNotFound)?; + + let customer_id = payment_intent + .customer_id + .ok_or(ApiErrorResponse::CustomerNotFound)?; + + let payment_methods = db + .find_payment_method_by_customer_id_merchant_id_list( + &customer_id, + &merchant_account.merchant_id, + ) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + let mut hash_to_payment_method: HashMap< + String, + ( + storage::PaymentMethod, + payment_methods::PaymentMethodDataBankCreds, + ), + > = HashMap::new(); + + for pm in payment_methods { + if pm.payment_method == enums::PaymentMethod::BankDebit { + let bank_details_pm_data = decrypt::( + pm.payment_method_data.clone(), + key, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("unable to decrypt bank account details")? + .map(|x| x.into_inner().expose()) + .map(|v| { + serde_json::from_value::(v) + .into_report() + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize Payment Method Auth config") + }) + .transpose() + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }) + .and_then(|pmd| match pmd { + payment_methods::PaymentMethodsData::BankDetails(bank_creds) => Some(bank_creds), + _ => None, + }) + .ok_or(ApiErrorResponse::InternalServerError)?; + + hash_to_payment_method.insert( + bank_details_pm_data.hash.clone(), + (pm, bank_details_pm_data), + ); + } + } + + #[cfg(feature = "kms")] + let pm_auth_key = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(state.conf.payment_method_auth.pm_auth_key.clone()) + .await + .change_context(ApiErrorResponse::InternalServerError)?; + + #[cfg(not(feature = "kms"))] + let pm_auth_key = state.conf.payment_method_auth.pm_auth_key.clone(); + + let mut update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)> = + Vec::new(); + let mut new_entries: Vec = Vec::new(); + + for creds in bank_account_details_resp.credentials { + let hash_string = format!("{}-{}", creds.account_number, creds.routing_number); + let generated_hash = hex::encode( + HmacSha256::sign_message(&HmacSha256, pm_auth_key.as_bytes(), hash_string.as_bytes()) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to sign the message")?, + ); + + let contains_account = hash_to_payment_method.get(&generated_hash); + let mut pmd = payment_methods::PaymentMethodDataBankCreds { + mask: creds + .account_number + .chars() + .rev() + .take(4) + .collect::() + .chars() + .rev() + .collect::(), + hash: generated_hash, + account_type: creds.account_type, + account_name: creds.account_name, + payment_method_type: creds.payment_method_type, + connector_details: vec![payment_methods::BankAccountConnectorDetails { + connector: connector_name.to_string(), + mca_id: mca_id.clone(), + access_token: payment_methods::BankAccountAccessCreds::AccessToken( + access_token.clone(), + ), + account_id: creds.account_id, + }], + }; + + if let Some((pm, details)) = contains_account { + pmd.connector_details.extend( + details + .connector_details + .clone() + .into_iter() + .filter(|conn| conn.mca_id != mca_id), + ); + + let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd); + let encrypted_data = + cards::create_encrypted_payment_method_data(&key_store, Some(payment_method_data)) + .await + .ok_or(ApiErrorResponse::InternalServerError)?; + let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate { + payment_method_data: Some(encrypted_data), + }; + + update_entries.push((pm.clone(), pm_update)); + } else { + let payment_method_data = payment_methods::PaymentMethodsData::BankDetails(pmd); + let encrypted_data = + cards::create_encrypted_payment_method_data(&key_store, Some(payment_method_data)) + .await + .ok_or(ApiErrorResponse::InternalServerError)?; + let pm_id = generate_id(consts::ID_LENGTH, "pm"); + let pm_new = storage::PaymentMethodNew { + customer_id: customer_id.clone(), + merchant_id: merchant_account.merchant_id.clone(), + payment_method_id: pm_id, + payment_method: enums::PaymentMethod::BankDebit, + payment_method_type: Some(creds.payment_method_type), + payment_method_issuer: None, + scheme: None, + metadata: None, + payment_method_data: Some(encrypted_data), + ..storage::PaymentMethodNew::default() + }; + + new_entries.push(pm_new); + }; + } + + store_in_db(update_entries, new_entries, db).await?; + + Ok(()) +} + +async fn store_in_db( + update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)>, + new_entries: Vec, + db: &dyn StorageInterface, +) -> RouterResult<()> { + let update_entries_futures = update_entries + .into_iter() + .map(|(pm, pm_update)| db.update_payment_method(pm, pm_update)) + .collect::>(); + + let new_entries_futures = new_entries + .into_iter() + .map(|pm_new| db.insert_payment_method(pm_new)) + .collect::>(); + + let update_futures = futures::future::join_all(update_entries_futures); + let new_futures = futures::future::join_all(new_entries_futures); + + let (update, new) = tokio::join!(update_futures, new_futures); + + let _ = update + .into_iter() + .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}"))); + + let _ = new + .into_iter() + .map(|res| res.map_err(|err| logger::error!("Payment method storage failed {err:?}"))); + + Ok(()) +} + +pub async fn get_bank_account_creds( + connector: PaymentAuthConnectorData, + merchant_account: &domain::MerchantAccount, + connector_name: &str, + access_token: &str, + auth_type: pm_auth_types::ConnectorAuthType, + state: &AppState, + bank_account_id: Option, +) -> RouterResult { + let connector_integration_bank_details: BoxedConnectorIntegration< + '_, + BankAccountCredentials, + pm_auth_types::BankAccountCredentialsRequest, + pm_auth_types::BankAccountCredentialsResponse, + > = connector.connector.get_connector_integration(); + + let router_data_bank_details = pm_auth_types::BankDetailsRouterData { + flow: std::marker::PhantomData, + merchant_id: Some(merchant_account.merchant_id.clone()), + connector: Some(connector_name.to_string()), + request: pm_auth_types::BankAccountCredentialsRequest { + access_token: access_token.to_string(), + optional_ids: bank_account_id + .map(|id| pm_auth_types::BankAccountOptionalIDs { ids: vec![id] }), + }, + response: Ok(pm_auth_types::BankAccountCredentialsResponse { + credentials: Vec::new(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type, + }; + + let bank_details_resp = pm_auth_services::execute_connector_processing_step( + state, + connector_integration_bank_details, + &router_data_bank_details, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling bank account details connector api")?; + + let bank_account_details_resp = + bank_details_resp + .response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + Ok(bank_account_details_resp) +} + +async fn get_access_token_from_exchange_api( + connector: &PaymentAuthConnectorData, + connector_name: &str, + payload: &api_models::pm_auth::ExchangeTokenCreateRequest, + auth_type: &pm_auth_types::ConnectorAuthType, + state: &AppState, +) -> RouterResult { + let connector_integration: BoxedConnectorIntegration< + '_, + ExchangeToken, + pm_auth_types::ExchangeTokenRequest, + pm_auth_types::ExchangeTokenResponse, + > = connector.connector.get_connector_integration(); + + let router_data = pm_auth_types::ExchangeTokenRouterData { + flow: std::marker::PhantomData, + merchant_id: None, + connector: Some(connector_name.to_string()), + request: pm_auth_types::ExchangeTokenRequest { + public_token: payload.public_token.clone(), + }, + response: Ok(pm_auth_types::ExchangeTokenResponse { + access_token: "".to_string(), + }), + connector_http_status_code: None, + connector_auth_type: auth_type.clone(), + }; + + let resp = pm_auth_services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + &connector.connector_name, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while calling exchange token connector api")?; + + let exchange_token_resp = + resp.response + .map_err(|err| ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector.connector_name.to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + + let access_token = exchange_token_resp.access_token; + Ok(access_token) +} + +async fn get_selected_config_from_redis( + db: &dyn StorageInterface, + payload: &api_models::pm_auth::ExchangeTokenCreateRequest, +) -> RouterResult { + let redis_conn = db + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + + let pm_auth_key = format!("pm_auth_{}", payload.payment_id); + + let pm_auth_configs = redis_conn + .get_and_deserialize_key::>( + pm_auth_key.as_str(), + "Vec", + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get payment method auth choices from redis")?; + + let selected_config = pm_auth_configs + .iter() + .find(|conf| { + conf.payment_method == payload.payment_method + && conf.payment_method_type == payload.payment_method_type + }) + .ok_or(ApiErrorResponse::GenericNotFoundError { + message: "connector name not found".to_string(), + }) + .into_report()? + .clone(); + + Ok(selected_config) +} + +pub async fn retrieve_payment_method_from_auth_service( + state: &AppState, + key_store: &domain::MerchantKeyStore, + auth_token: &payment_methods::BankAccountConnectorDetails, + payment_intent: &PaymentIntent, +) -> RouterResult> { + let db = state.store.as_ref(); + + let connector = pm_auth_types::api::PaymentAuthConnectorData::get_connector_by_name( + auth_token.connector.as_str(), + )?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(&payment_intent.merchant_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let mca = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &payment_intent.merchant_id, + &auth_token.mca_id, + key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: auth_token.mca_id.clone(), + }) + .attach_printable( + "error while fetching merchant_connector_account from merchant_id and connector name", + )?; + + let auth_type = pm_auth_helpers::get_connector_auth_type(mca)?; + + let BankAccountAccessCreds::AccessToken(access_token) = &auth_token.access_token; + + let bank_account_creds = get_bank_account_creds( + connector, + &merchant_account, + &auth_token.connector, + access_token, + auth_type, + state, + Some(auth_token.account_id.clone()), + ) + .await?; + + logger::debug!("bank_creds: {:?}", bank_account_creds); + + let bank_account = bank_account_creds + .credentials + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Bank account details not found")?; + + let mut bank_type = None; + if let Some(account_type) = bank_account.account_type.clone() { + bank_type = api_models::enums::BankType::from_str(account_type.as_str()) + .map_err(|error| logger::error!(%error,"unable to parse account_type {account_type:?}")) + .ok(); + } + + let address = oss_helpers::get_address_by_id( + &*state.store, + payment_intent.billing_address_id.clone(), + key_store, + payment_intent.payment_id.clone(), + merchant_account.merchant_id.clone(), + merchant_account.storage_scheme, + ) + .await?; + + let name = address + .as_ref() + .and_then(|addr| addr.first_name.clone().map(|name| name.into_inner())); + + let address_details = address.clone().map(|addr| { + let line1 = addr.line1.map(|line1| line1.into_inner()); + let line2 = addr.line2.map(|line2| line2.into_inner()); + let line3 = addr.line3.map(|line3| line3.into_inner()); + let zip = addr.zip.map(|zip| zip.into_inner()); + let state = addr.state.map(|state| state.into_inner()); + let first_name = addr.first_name.map(|first_name| first_name.into_inner()); + let last_name = addr.last_name.map(|last_name| last_name.into_inner()); + + AddressDetails { + city: addr.city, + country: addr.country, + line1, + line2, + line3, + zip, + state, + first_name, + last_name, + } + }); + let payment_method_data = PaymentMethodData::BankDebit(BankDebitData::AchBankDebit { + billing_details: BankDebitBilling { + name: name.unwrap_or_default(), + email: common_utils::pii::Email::from(masking::Secret::new("".to_string())), + address: address_details, + }, + account_number: masking::Secret::new(bank_account.account_number.clone()), + routing_number: masking::Secret::new(bank_account.routing_number.clone()), + card_holder_name: None, + bank_account_holder_name: None, + bank_name: None, + bank_type, + bank_holder_type: None, + }); + + Ok(Some((payment_method_data, enums::PaymentMethod::BankDebit))) +} diff --git a/crates/router/src/core/pm_auth/helpers.rs b/crates/router/src/core/pm_auth/helpers.rs new file mode 100644 index 000000000000..43d30705a803 --- /dev/null +++ b/crates/router/src/core/pm_auth/helpers.rs @@ -0,0 +1,33 @@ +use common_utils::ext_traits::ValueExt; +use error_stack::{IntoReport, ResultExt}; +use pm_auth::types::{self as pm_auth_types, api::BoxedPaymentAuthConnector}; + +use crate::{ + core::errors::{self, ApiErrorResponse}, + types::{self, domain, transformers::ForeignTryFrom}, +}; + +pub trait PaymentAuthConnectorDataExt { + fn get_connector_by_name(name: &str) -> errors::CustomResult + where + Self: Sized; + fn convert_connector( + connector_name: pm_auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult; +} + +pub fn get_connector_auth_type( + merchant_connector_account: domain::MerchantConnectorAccount, +) -> errors::CustomResult { + let auth_type: types::ConnectorAuthType = merchant_connector_account + .connector_account_details + .parse_value("ConnectorAuthType") + .change_context(ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + pm_auth_types::ConnectorAuthType::foreign_try_from(auth_type) + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while converting ConnectorAuthType") +} diff --git a/crates/router/src/core/pm_auth/transformers.rs b/crates/router/src/core/pm_auth/transformers.rs new file mode 100644 index 000000000000..8a1369c2e02f --- /dev/null +++ b/crates/router/src/core/pm_auth/transformers.rs @@ -0,0 +1,18 @@ +use pm_auth::types::{self as pm_auth_types}; + +use crate::{core::errors, types, types::transformers::ForeignTryFrom}; + +impl ForeignTryFrom for pm_auth_types::ConnectorAuthType { + type Error = errors::ConnectorError; + fn foreign_try_from(auth_type: types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => { + Ok::(Self::BodyKey { + client_id: api_key.to_owned(), + secret: key1.to_owned(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType), + } + } +} diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ce1717c9e936..ec718b2dde9f 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -40,6 +40,8 @@ pub mod verify_connector; pub mod webhooks; pub mod locker_migration; +#[cfg(any(feature = "olap", feature = "oltp"))] +pub mod pm_auth; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(any(feature = "olap", feature = "oltp"))] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 6b72e69b9f4e..a7c394b7b6c1 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -20,6 +20,8 @@ use super::currency; use super::dummy_connector::*; #[cfg(feature = "payouts")] use super::payouts::*; +#[cfg(feature = "oltp")] +use super::pm_auth; #[cfg(feature = "olap")] use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] @@ -555,6 +557,8 @@ impl PaymentMethods { .route(web::post().to(payment_method_update_api)) .route(web::delete().to(payment_method_delete_api)), ) + .service(web::resource("/auth/link").route(web::post().to(pm_auth::link_token_create))) + .service(web::resource("/auth/exchange").route(web::post().to(pm_auth::exchange_token))) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 88c35bb0a13d..533d1d3a6297 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -13,6 +13,7 @@ pub enum ApiIdentifier { Ephemeral, Mandates, PaymentMethods, + PaymentMethodAuth, Payouts, Disputes, CardsInfo, @@ -86,6 +87,8 @@ impl From for ApiIdentifier { | Flow::PaymentMethodsDelete | Flow::ValidatePaymentMethod => Self::PaymentMethods, + Flow::PmAuthLinkTokenCreate | Flow::PmAuthExchangeToken => Self::PaymentMethodAuth, + Flow::PaymentsCreate | Flow::PaymentsRetrieve | Flow::PaymentsUpdate diff --git a/crates/router/src/routes/pm_auth.rs b/crates/router/src/routes/pm_auth.rs new file mode 100644 index 000000000000..cfadd787c310 --- /dev/null +++ b/crates/router/src/routes/pm_auth.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpRequest, Responder}; +use api_models as api_types; +use router_env::{instrument, tracing, types::Flow}; + +use crate::{core::api_locking, routes::AppState, services::api as oss_api}; + +#[instrument(skip_all, fields(flow = ?Flow::PmAuthLinkTokenCreate))] +pub async fn link_token_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + let flow = Flow::PmAuthLinkTokenCreate; + let (auth, _) = match crate::services::authentication::check_client_secret_and_get_auth( + req.headers(), + &payload, + ) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return oss_api::log_and_return_error_response(e), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| { + crate::core::pm_auth::create_link_token( + state, + auth.merchant_account, + auth.key_store, + payload, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::PmAuthExchangeToken))] +pub async fn exchange_token( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let payload = json_payload.into_inner(); + let flow = Flow::PmAuthExchangeToken; + let (auth, _) = match crate::services::authentication::check_client_secret_and_get_auth( + req.headers(), + &payload, + ) { + Ok((auth, _auth_flow)) => (auth, _auth_flow), + Err(e) => return oss_api::log_and_return_error_response(e), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, payload| { + crate::core::pm_auth::exchange_token_core( + state, + auth.merchant_account, + auth.key_store, + payload, + ) + }, + &*auth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index e46612b95dfc..57f3b802bd5d 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -6,6 +6,7 @@ pub mod encryption; pub mod jwt; pub mod kafka; pub mod logger; +pub mod pm_auth; #[cfg(feature = "email")] pub mod email; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 8a0cd7c729e9..b48465ebd174 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -641,6 +641,18 @@ impl ClientSecretFetch for api_models::payments::RetrievePaymentLinkRequest { } } +impl ClientSecretFetch for api_models::pm_auth::LinkTokenCreateRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + +impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + pub fn get_auth_type_and_flow( headers: &HeaderMap, ) -> RouterResult<( diff --git a/crates/router/src/services/pm_auth.rs b/crates/router/src/services/pm_auth.rs new file mode 100644 index 000000000000..7487b12663b1 --- /dev/null +++ b/crates/router/src/services/pm_auth.rs @@ -0,0 +1,95 @@ +use pm_auth::{ + consts, + core::errors::ConnectorError, + types::{self as pm_auth_types, api::BoxedConnectorIntegration, PaymentAuthRouterData}, +}; + +use crate::{ + core::errors::{self}, + logger, + routes::AppState, + services::{self}, +}; + +pub async fn execute_connector_processing_step< + 'b, + 'a, + T: 'static, + Req: Clone + 'static, + Resp: Clone + 'static, +>( + state: &'b AppState, + connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>, + req: &'b PaymentAuthRouterData, + connector: &pm_auth_types::PaymentMethodAuthConnectors, +) -> errors::CustomResult, ConnectorError> +where + T: Clone, + Req: Clone, + Resp: Clone, +{ + let mut router_data = req.clone(); + + let connector_request = connector_integration.build_request(req, connector)?; + + match connector_request { + Some(request) => { + logger::debug!(connector_request=?request); + let response = services::api::call_connector_api(state, request).await; + logger::debug!(connector_response=?response); + match response { + Ok(body) => { + let response = match body { + Ok(body) => { + let body = pm_auth_types::Response { + headers: body.headers, + response: body.response, + status_code: body.status_code, + }; + let connector_http_status_code = Some(body.status_code); + let mut data = + connector_integration.handle_response(&router_data, body)?; + data.connector_http_status_code = connector_http_status_code; + + data + } + Err(body) => { + let body = pm_auth_types::Response { + headers: body.headers, + response: body.response, + status_code: body.status_code, + }; + router_data.connector_http_status_code = Some(body.status_code); + + let error = match body.status_code { + 500..=511 => connector_integration.get_5xx_error_response(body)?, + _ => connector_integration.get_error_response(body)?, + }; + + router_data.response = Err(error); + + router_data + } + }; + Ok(response) + } + Err(error) => { + if error.current_context().is_upstream_timeout() { + let error_response = pm_auth_types::ErrorResponse { + code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(), + message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), + reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), + status_code: 504, + }; + router_data.response = Err(error_response); + router_data.connector_http_status_code = Some(504); + Ok(router_data) + } else { + Err(error.change_context(ConnectorError::ProcessingStepFailed(None))) + } + } + } + } + None => Ok(router_data), + } +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index de28c1a3188c..aa563c647eaa 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -10,6 +10,8 @@ pub mod api; pub mod domain; #[cfg(feature = "frm")] pub mod fraud_check; +pub mod pm_auth; + pub mod storage; pub mod transformers; diff --git a/crates/router/src/types/pm_auth.rs b/crates/router/src/types/pm_auth.rs new file mode 100644 index 000000000000..e2d08c6afeac --- /dev/null +++ b/crates/router/src/types/pm_auth.rs @@ -0,0 +1,38 @@ +use std::str::FromStr; + +use error_stack::{IntoReport, ResultExt}; +use pm_auth::{ + connector::plaid, + types::{ + self as pm_auth_types, + api::{BoxedPaymentAuthConnector, PaymentAuthConnectorData}, + }, +}; + +use crate::core::{ + errors::{self, ApiErrorResponse}, + pm_auth::helpers::PaymentAuthConnectorDataExt, +}; + +impl PaymentAuthConnectorDataExt for PaymentAuthConnectorData { + fn get_connector_by_name(name: &str) -> errors::CustomResult { + let connector_name = pm_auth_types::PaymentMethodAuthConnectors::from_str(name) + .into_report() + .change_context(ApiErrorResponse::IncorrectConnectorNameGiven) + .attach_printable_lazy(|| { + format!("unable to parse connector: {:?}", name.to_string()) + })?; + let connector = Self::convert_connector(connector_name.clone())?; + Ok(Self { + connector, + connector_name, + }) + } + fn convert_connector( + connector_name: pm_auth_types::PaymentMethodAuthConnectors, + ) -> errors::CustomResult { + match connector_name { + pm_auth_types::PaymentMethodAuthConnectors::Plaid => Ok(Box::new(&plaid::Plaid)), + } + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 13ca344e9c57..b682bcb12e66 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -295,6 +295,10 @@ pub enum Flow { UserMerchantAccountList, /// Get users for merchant account GetUserDetails, + /// PaymentMethodAuth Link token create + PmAuthLinkTokenCreate, + /// PaymentMethodAuth Exchange token create + PmAuthExchangeToken, /// Get reset password link ForgotPassword, /// Reset password using link From b6838c4d1a3a456e28a5f438fcd74a60bedb2539 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Wed, 6 Dec 2023 22:52:50 +0530 Subject: [PATCH 161/443] docs(openapi): fix `payment_methods_enabled` OpenAPI spec in merchant connector account APIs (#3068) --- crates/api_models/src/admin.rs | 2 +- crates/api_models/src/payment_methods.rs | 7 ++- crates/router/src/openapi.rs | 1 + openapi/openapi_spec.json | 72 +++++++++++++++++++++++- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 6bb4fd4afa0f..d35b12152e91 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -879,7 +879,7 @@ pub struct PaymentMethodsEnabled { pub payment_method: common_enums::PaymentMethod, /// Subtype of payment method - #[schema(value_type = Option>,example = json!(["credit"]))] + #[schema(value_type = Option>,example = json!(["credit"]))] pub payment_method_types: Option>, } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 84830498b344..85b0adefca5f 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -413,8 +413,11 @@ impl ResponsePaymentMethodIntermediate { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq, Hash)] pub struct RequestPaymentMethodTypes { + #[schema(value_type = PaymentMethodType)] pub payment_method_type: api_enums::PaymentMethodType, + #[schema(value_type = Option)] pub payment_experience: Option, + #[schema(value_type = Option>)] pub card_networks: Option>, /// List of currencies accepted or has the processing capabilities of the processor #[schema(example = json!( @@ -422,7 +425,7 @@ pub struct RequestPaymentMethodTypes { "type": "specific_accepted", "list": ["USD", "INR"] } - ))] + ), value_type = Option)] pub accepted_currencies: Option, /// List of Countries accepted or has the processing capabilities of the processor @@ -431,7 +434,7 @@ pub struct RequestPaymentMethodTypes { "type": "specific_accepted", "list": ["UK", "AU"] } - ))] + ), value_type = Option)] pub accepted_countries: Option, /// Minimum amount supported by the processor. To be represented in the lowest denomination of the target currency (For example, for USD it should be in cents) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index d83117c59d76..95c36719cad1 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -322,6 +322,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::SurchargeDetailsResponse, api_models::payment_methods::SurchargeResponse, api_models::payment_methods::SurchargePercentage, + api_models::payment_methods::RequestPaymentMethodTypes, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index d67089aea35f..f77638a43db5 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9312,7 +9312,7 @@ "payment_method_types": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/RequestPaymentMethodTypes" }, "description": "Subtype of payment method", "example": [ @@ -11584,6 +11584,76 @@ } } }, + "RequestPaymentMethodTypes": { + "type": "object", + "required": [ + "payment_method_type", + "recurring_enabled", + "installment_payment_enabled" + ], + "properties": { + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true + }, + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetwork" + }, + "nullable": true + }, + "accepted_currencies": { + "allOf": [ + { + "$ref": "#/components/schemas/AcceptedCurrencies" + } + ], + "nullable": true + }, + "accepted_countries": { + "allOf": [ + { + "$ref": "#/components/schemas/AcceptedCountries" + } + ], + "nullable": true + }, + "minimum_amount": { + "type": "integer", + "format": "int32", + "description": "Minimum amount supported by the processor. To be represented in the lowest denomination of the target currency (For example, for USD it should be in cents)", + "example": 1, + "nullable": true + }, + "maximum_amount": { + "type": "integer", + "format": "int32", + "description": "Maximum amount supported by the processor. To be represented in the lowest denomination of\nthe target currency (For example, for USD it should be in cents)", + "example": 1313, + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Boolean to enable recurring payments / mandates. Default is true.", + "default": true, + "example": false + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Boolean to enable installment / EMI / BNPL payments. Default is true.", + "default": true, + "example": false + } + } + }, "RequestSurchargeDetails": { "type": "object", "required": [ From f53b090db87e094f9694481f13af62240c4c422a Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:54:16 +0530 Subject: [PATCH 162/443] feat(connector): accept connector_transaction_id in error_response of connector flows for Trustpay (#3060) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/trustpay.rs | 20 ++++---------- .../src/connector/trustpay/transformers.rs | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 2430aac6c19f..286eaf9cb542 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -137,9 +137,11 @@ impl ConnectorCommon for Trustpay { message: option_error_code_message .map(|error_code_message| error_code_message.error_code) .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: reason.or(response_data.description), + reason: reason + .or(response_data.description) + .or(response_data.payment_description), attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: response_data.instance_id, }) } Err(error_msg) => { @@ -363,19 +365,7 @@ impl ConnectorIntegration CustomResult { - let response: trustpay::TrustPayTransactionStatusErrorResponse = res - .response - .parse_struct("trustpay transaction status ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.to_string(), - // message vary for the same code, so relying on code alone as it is unique - message: response.status.to_string(), - reason: Some(response.payment_description), - attempt_status: None, - connector_transaction_id: None, - }) + self.build_error_response(res) } fn handle_response( diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index e985eff11976..270a702bd6ec 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -722,13 +722,13 @@ fn handle_cards_response( reason: msg, status_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(response.instance_id.clone()), }) } else { None }; let payment_response_data = types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(response.instance_id), + resource_id: types::ResponseId::ConnectorTransactionId(response.instance_id.clone()), redirection_data, mandate_reference: None, connector_metadata: None, @@ -825,14 +825,24 @@ fn handle_bank_redirects_sync_response( reason: reason_info.reason.reject_reason, status_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some( + response + .payment_information + .references + .payment_request_id + .clone(), + ), }) } else { None }; let payment_response_data = types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - response.payment_information.references.payment_request_id, + response + .payment_information + .references + .payment_request_id + .clone(), ), redirection_data: None, mandate_reference: None, @@ -1637,16 +1647,13 @@ pub struct Errors { } #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct TrustpayErrorResponse { pub status: i64, pub description: Option, pub errors: Option>, -} - -#[derive(Deserialize)] -pub struct TrustPayTransactionStatusErrorResponse { - pub status: i64, - pub payment_description: String, + pub instance_id: Option, + pub payment_description: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] From 585e00980c43797f326efb809df9ffd497d1dd26 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:16:03 +0530 Subject: [PATCH 163/443] feat(user): Add `verify_email` API (#3076) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 5 +-- crates/api_models/src/user.rs | 7 ++++ crates/router/src/core/user.rs | 39 ++++++++++++++++++++--- crates/router/src/routes/app.rs | 15 +++++---- crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/user.rs | 21 +++++++++++- crates/router/src/services/email/types.rs | 14 ++++---- crates/router_env/src/logger/types.rs | 2 ++ 8 files changed, 84 insertions(+), 22 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index ca2932725317..92b675723964 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -9,7 +9,7 @@ use crate::user::{ AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, InviteUserResponse, ResetPasswordRequest, SignUpRequest, SignUpWithMerchantIdRequest, - SwitchMerchantIdRequest, UserMerchantCreate, + SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, }; impl ApiEventMetric for DashboardEntryResponse { @@ -38,7 +38,8 @@ common_utils::impl_misc_api_event_type!( ForgotPasswordRequest, ResetPasswordRequest, InviteUserRequest, - InviteUserResponse + InviteUserResponse, + VerifyEmailRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e5f06fdbfae3..10d8411f8e70 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -121,3 +121,10 @@ pub struct UserDetails { #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: time::PrimitiveDateTime, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct VerifyEmailRequest { + pub token: Secret, +} + +pub type VerifyEmailResponse = DashboardEntryResponse; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 01947d08d1f9..f44042a2dcca 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -11,7 +11,7 @@ use router_env::logger; use super::errors::{UserErrors, UserResponse}; #[cfg(feature = "email")] -use crate::services::email::{types as email_types, types::EmailToken}; +use crate::services::email::types as email_types; use crate::{ consts, db::user::UserInterface, @@ -296,9 +296,10 @@ pub async fn reset_password( state: AppState, request: user_api::ResetPasswordRequest, ) -> UserResponse<()> { - let token = auth::decode_jwt::(request.token.expose().as_str(), &state) - .await - .change_context(UserErrors::LinkInvalid)?; + let token = + auth::decode_jwt::(request.token.expose().as_str(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; let password = domain::UserPassword::new(request.password)?; @@ -630,3 +631,33 @@ pub async fn get_users_for_merchant_account( Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) } + +#[cfg(feature = "email")] +pub async fn verify_email( + state: AppState, + req: user_api::VerifyEmailRequest, +) -> UserResponse { + let token = auth::decode_jwt::(&req.token.clone().expose(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + let user = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)?; + + let user = state + .store + .update_user_by_user_id(user.user_id.as_str(), storage_user::UserUpdate::VerifyUser) + .await + .change_context(UserErrors::InternalServerError)?; + + let user_from_db: domain::UserFromStorage = user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let jwt_token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, jwt_token), + )) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a7c394b7b6c1..34ae6fa3ecb0 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -863,11 +863,6 @@ impl User { route = route .service(web::resource("/signin").route(web::post().to(user_signin))) .service(web::resource("/change_password").route(web::post().to(change_password))) - .service( - web::resource("/data/merchant") - .route(web::post().to(set_merchant_scoped_dashboard_metadata)), - ) - .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) .service( @@ -879,7 +874,12 @@ impl User { .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) - .service(web::resource("/role/{role_id}").route(web::get().to(get_role))); + .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) + .service( + web::resource("/data") + .route(web::get().to(get_multiple_dashboard_metadata)) + .route(web::post().to(set_dashboard_metadata)), + ); #[cfg(feature = "dummy_connector")] { @@ -901,7 +901,8 @@ impl User { .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), - ); + ) + .service(web::resource("/verify_email").route(web::post().to(verify_email))) } #[cfg(not(feature = "email"))] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 533d1d3a6297..f5519b960375 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -170,7 +170,8 @@ impl From for ApiIdentifier { | Flow::ForgotPassword | Flow::ResetPassword | Flow::InviteUser - | Flow::UserSignUpWithMerchantId => Self::User, + | Flow::UserSignUpWithMerchantId + | Flow::VerifyEmail => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index c4476d6ed710..594da67aa023 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -115,7 +115,7 @@ pub async fn change_password( .await } -pub async fn set_merchant_scoped_dashboard_metadata( +pub async fn set_dashboard_metadata( state: web::Data, req: HttpRequest, json_payload: web::Json, @@ -351,3 +351,22 @@ pub async fn invite_user( )) .await } + +#[cfg(feature = "email")] +pub async fn verify_email( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmail; + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, _, req_payload| user_core::verify_email(state, req_payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index ad91edd8c364..9e26c45ba6b1 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -50,7 +50,7 @@ pub mod html { #[derive(serde::Serialize, serde::Deserialize)] pub struct EmailToken { email: String, - expiration: u64, + exp: u64, } impl EmailToken { @@ -59,10 +59,10 @@ impl EmailToken { settings: &configs::settings::Settings, ) -> CustomResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); - let expiration = jwt::generate_exp(expiration_duration)?.as_secs(); + let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let token_payload = Self { email: email.get_secret().expose(), - expiration, + exp, }; jwt::generate_jwt(&token_payload, settings).await } @@ -95,7 +95,7 @@ impl EmailData for VerifyEmail { .change_context(EmailError::TokenGenerationFailure)?; let verify_email_link = - get_link_with_token(&self.settings.server.base_url, token, "verify_email"); + get_link_with_token(&self.settings.email.base_url, token, "verify_email"); let body = html::get_html_body(EmailBody::Verify { link: verify_email_link, @@ -124,7 +124,7 @@ impl EmailData for ResetPassword { .change_context(EmailError::TokenGenerationFailure)?; let reset_password_link = - get_link_with_token(&self.settings.server.base_url, token, "set_password"); + get_link_with_token(&self.settings.email.base_url, token, "set_password"); let body = html::get_html_body(EmailBody::Reset { link: reset_password_link, @@ -153,7 +153,7 @@ impl EmailData for MagicLink { .await .change_context(EmailError::TokenGenerationFailure)?; - let magic_link_login = get_link_with_token(&self.settings.server.base_url, token, "login"); + let magic_link_login = get_link_with_token(&self.settings.email.base_url, token, "login"); let body = html::get_html_body(EmailBody::MagicLink { link: magic_link_login, @@ -183,7 +183,7 @@ impl EmailData for InviteUser { .change_context(EmailError::TokenGenerationFailure)?; let invite_user_link = - get_link_with_token(&self.settings.server.base_url, token, "set_password"); + get_link_with_token(&self.settings.email.base_url, token, "set_password"); let body = html::get_html_body(EmailBody::MagicLink { link: invite_user_link, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b682bcb12e66..6344c21f5f89 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -311,6 +311,8 @@ pub enum Flow { GetActionUrl, /// Sync connector onboarding status SyncOnboardingStatus, + /// Verify email Token + VerifyEmail, } /// From 26a261131b4dbb8570e139127a2c0d356e2820be Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:17:46 +0530 Subject: [PATCH 164/443] fix(user): add checks for change password (#3078) Co-authored-by: Rachit Naithani <81706961+racnan@users.noreply.github.com> --- crates/router/src/core/errors/user.rs | 8 +++++++ .../src/core/errors/user/sample_data.rs | 24 ++++--------------- crates/router/src/core/user.rs | 10 ++++++-- crates/router/src/utils/user/sample_data.rs | 2 +- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 9a5308852229..eaa80c07b185 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -48,6 +48,8 @@ pub enum UserErrors { InvalidMetadataRequest, #[error("MerchantIdParsingError")] MerchantIdParsingError, + #[error("ChangePasswordError")] + ChangePasswordError, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -136,6 +138,12 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None)) } + Self::ChangePasswordError => AER::BadRequest(ApiError::new( + sub_code, + 29, + "Old and new password cannot be same", + None, + )), } } } diff --git a/crates/router/src/core/errors/user/sample_data.rs b/crates/router/src/core/errors/user/sample_data.rs index 11233b27b5cd..84c6c9fa43a2 100644 --- a/crates/router/src/core/errors/user/sample_data.rs +++ b/crates/router/src/core/errors/user/sample_data.rs @@ -10,10 +10,6 @@ pub enum SampleDataError { InternalServerError, #[error("Data Does Not Exist")] DataDoesNotExist, - #[error("Server Error")] - DatabaseError, - #[error("Merchant Id Not Found")] - MerchantIdNotFound, #[error("Invalid Parameters")] InvalidParameters, #[error["Invalid Records"]] @@ -29,33 +25,21 @@ impl ErrorSwitch for SampleDataError { "Something went wrong", None, )), - Self::DatabaseError => ApiErrorResponse::InternalServerError(ApiError::new( - "SD", - 1, - "Server Error(DB is down)", - None, - )), Self::DataDoesNotExist => ApiErrorResponse::NotFound(ApiError::new( "SD", - 2, + 1, "Sample Data not present for given request", None, )), - Self::MerchantIdNotFound => ApiErrorResponse::BadRequest(ApiError::new( - "SD", - 3, - "Merchant ID not provided", - None, - )), Self::InvalidParameters => ApiErrorResponse::BadRequest(ApiError::new( "SD", - 4, + 2, "Invalid parameters to generate Sample Data", None, )), Self::InvalidRange => ApiErrorResponse::BadRequest(ApiError::new( "SD", - 5, + 3, "Records to be generated should be between range 10 and 100", None, )), @@ -67,7 +51,7 @@ impl ErrorSwitchFrom for SampleDataError { fn switch_from(error: &StorageError) -> Self { match matches!(error, StorageError::ValueNotFound(_)) { true => Self::DataDoesNotExist, - false => Self::DatabaseError, + false => Self::InternalServerError, } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index f44042a2dcca..83292771174e 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -232,10 +232,16 @@ pub async fn change_password( .change_context(UserErrors::InternalServerError)? .into(); - user.compare_password(request.old_password) + user.compare_password(request.old_password.to_owned()) .change_context(UserErrors::InvalidOldPassword)?; - let new_password_hash = utils::user::password::generate_password_hash(request.new_password)?; + if request.old_password == request.new_password { + return Err(UserErrors::ChangePasswordError.into()); + } + let new_password = domain::UserPassword::new(request.new_password)?; + + let new_password_hash = + utils::user::password::generate_password_hash(new_password.get_secret())?; let _ = UserInterface::update_user_by_user_id( &*state.store, diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 543e3cd2aa5f..9f95e2d078dd 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -34,7 +34,7 @@ pub async fn generate_sample_data( &state.store.get_master_key().to_vec().into(), ) .await - .change_context(SampleDataError::DatabaseError)?; + .change_context(SampleDataError::InternalServerError)?; let merchant_from_db = state .store From 42b5bd4f3d142c9fa12475f36a8b144753ac06e2 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Thu, 7 Dec 2023 16:30:28 +0530 Subject: [PATCH 165/443] fix(drainer): properly log deserialization errors (#3075) --- crates/drainer/src/lib.rs | 11 ++++++++--- crates/drainer/src/metrics.rs | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index 7b77873a648e..796c9aa69550 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -7,7 +7,7 @@ pub mod settings; mod utils; use std::sync::{atomic, Arc}; -use common_utils::signals::get_allowed_signals; +use common_utils::{ext_traits::StringExt, signals::get_allowed_signals}; use diesel_models::kv; use error_stack::{IntoReport, ResultExt}; use router_env::{instrument, tracing}; @@ -205,10 +205,15 @@ async fn drainer( tracing::Span::current().record("global_id", global_id); tracing::Span::current().record("session_id", &session_id); - let result = serde_json::from_str::(&typed_sql); + let result = typed_sql.parse_struct("DBOperation"); + let db_op = match result { Ok(f) => f, - Err(_err) => continue, // TODO: handle error + Err(err) => { + logger::error!(operation= "deserialization",error = %err); + metrics::STREAM_PARSE_FAIL.add(&metrics::CONTEXT, 1, &[]); + continue; + } }; let conn = pg_connection(&store.master_pool).await; diff --git a/crates/drainer/src/metrics.rs b/crates/drainer/src/metrics.rs index 06e9119787d5..750f23bc73b5 100644 --- a/crates/drainer/src/metrics.rs +++ b/crates/drainer/src/metrics.rs @@ -14,6 +14,7 @@ counter_metric!(SUCCESSFUL_QUERY_EXECUTION, DRAINER_METER); counter_metric!(SHUTDOWN_SIGNAL_RECEIVED, DRAINER_METER); counter_metric!(SUCCESSFUL_SHUTDOWN, DRAINER_METER); counter_metric!(STREAM_EMPTY, DRAINER_METER); +counter_metric!(STREAM_PARSE_FAIL, DRAINER_METER); counter_metric!(DRAINER_HEALTH, DRAINER_METER); histogram_metric!(QUERY_EXECUTION_TIME, DRAINER_METER); // Time in (ms) milliseconds From 1f8116db368aec344d08603045c4cb46c2c25b41 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:51:40 +0530 Subject: [PATCH 166/443] fix(config): add missing config fields in `docker_compose.toml` (#3080) --- config/docker_compose.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 5eec8d733d6a..7f9fc9eaad59 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -375,3 +375,7 @@ enabled = true client_id = "" client_secret = "" partner_id = "" +enabled = true + +[events] +source = "logs" From f7d6e3c0149869175a59996e67d3e2d3b6f3b8c2 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:55:47 +0530 Subject: [PATCH 167/443] refactor(user): add account verification check in signin (#3082) --- crates/router/src/core/errors/user.rs | 8 ++++++++ crates/router/src/core/user.rs | 15 +++++++++------ crates/router/src/types/domain/user.rs | 23 +++++++++++++++++++++++ crates/router/src/utils/user.rs | 15 +++++++++++---- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index eaa80c07b185..03ca88056101 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -18,6 +18,8 @@ pub enum UserErrors { UserExists, #[error("LinkInvalid")] LinkInvalid, + #[error("UnverifiedUser")] + UnverifiedUser, #[error("InvalidOldPassword")] InvalidOldPassword, #[error("EmailParsingError")] @@ -81,6 +83,12 @@ impl common_utils::errors::ErrorSwitch { AER::Unauthorized(ApiError::new(sub_code, 4, "Invalid or expired link", None)) } + Self::UnverifiedUser => AER::Unauthorized(ApiError::new( + sub_code, + 5, + "Kindly verify your account", + None, + )), Self::InvalidOldPassword => AER::BadRequest(ApiError::new( sub_code, 6, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 83292771174e..e2c538ca80ee 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -91,10 +91,11 @@ pub async fn signup( UserStatus::Active, ) .await?; - let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + let token = + utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, )) } @@ -118,10 +119,11 @@ pub async fn signin( user_from_db.compare_password(request.password)?; let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + let token = + utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, )) } @@ -661,9 +663,10 @@ pub async fn verify_email( let user_from_db: domain::UserFromStorage = user.into(); let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let jwt_token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + let token = + utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(user_from_db, user_role, jwt_token), + utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, )) } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 16a00f117034..9bc27cba2b1d 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -736,6 +736,29 @@ impl UserFromStorage { .await .change_context(UserErrors::InternalServerError) } + + #[cfg(feature = "email")] + pub fn get_verification_days_left(&self, state: AppState) -> UserResult> { + if self.0.is_verified { + return Ok(None); + } + + let allowed_unverified_duration = + time::Duration::days(state.conf.email.allowed_unverified_days); + + let user_created = self.0.created_at.date(); + let last_date_for_verification = user_created + .checked_add(allowed_unverified_duration) + .ok_or(UserErrors::InternalServerError)?; + + let today = common_utils::date_time::now().date(); + if today >= last_date_for_verification { + return Err(UserErrors::UnverifiedUser.into()); + } + + let days_left_for_verification = last_date_for_verification - today; + Ok(Some(days_left_for_verification.whole_days())) + } } impl TryFrom for user_role_api::ModuleInfo { diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 0403d9b453d0..5f765028014f 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -104,18 +104,25 @@ pub async fn generate_jwt_auth_token_with_custom_merchant_id( Ok(Secret::new(token)) } +#[allow(unused_variables)] pub fn get_dashboard_entry_response( + state: AppState, user: UserFromStorage, user_role: UserRole, token: Secret, -) -> user_api::DashboardEntryResponse { - user_api::DashboardEntryResponse { +) -> UserResult { + #[cfg(feature = "email")] + let verification_days_left = user.get_verification_days_left(state)?; + #[cfg(not(feature = "email"))] + let verification_days_left = None; + + Ok(user_api::DashboardEntryResponse { merchant_id: user_role.merchant_id, token, name: user.get_name(), email: user.get_email(), user_id: user.get_user_id().to_string(), - verification_days_left: None, + verification_days_left, user_role: user_role.role_id, - } + }) } From 777cd5cdc2342fb7195a06505647fa331725e1dd Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 7 Dec 2023 19:42:25 +0530 Subject: [PATCH 168/443] chore(configs): [CYBERSOURCE] Add mandate configs (#3085) --- config/config.example.toml | 2 +- config/development.toml | 4 ++-- config/docker_compose.toml | 4 ++-- loadtest/config/development.toml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 7a50c23f484d..eb2574c92ea5 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -372,7 +372,7 @@ slack_invite_url = "https://www.example.com/" # Slack invite url for hyperswit discord_invite_url = "https://www.example.com/" # Discord invite url for hyperswitch [mandates.supported_payment_methods] -card.credit = { connector_list = "stripe,adyen" } # Mandate supported payment method type and connector for card +card.credit = { connector_list = "stripe,adyen,cybersource" } # Mandate supported payment method type and connector for card wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit diff --git a/config/development.toml b/config/development.toml index 15acfdee9b74..dc5423366247 100644 --- a/config/development.toml +++ b/config/development.toml @@ -446,8 +446,8 @@ pay_later.klarna = { connector_list = "adyen" } wallet.google_pay = { connector_list = "stripe,adyen" } wallet.apple_pay = { connector_list = "stripe,adyen" } wallet.paypal = { connector_list = "adyen" } -card.credit = { connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } -card.debit = { connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } +card.credit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } +card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 7f9fc9eaad59..437df22e300e 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -317,8 +317,8 @@ pay_later.klarna = {connector_list = "adyen"} wallet.google_pay = {connector_list = "stripe,adyen"} wallet.apple_pay = {connector_list = "stripe,adyen"} wallet.paypal = {connector_list = "adyen"} -card.credit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} -card.debit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 788835dd29de..f4070beb943d 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -243,8 +243,8 @@ pay_later.klarna = {connector_list = "adyen"} wallet.google_pay = {connector_list = "stripe,adyen"} wallet.apple_pay = {connector_list = "stripe,adyen"} wallet.paypal = {connector_list = "adyen"} -card.credit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} -card.debit = {connector_list = "stripe,adyen,authorizedotnet,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} +card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} From bf674380d5c7e856d0bae75554326aa9017c0201 Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:45:17 +0530 Subject: [PATCH 169/443] fix(analytics): adding api_path to api logs event and to auditlogs api response (#3079) Co-authored-by: Sagar naik --- .../analytics/docs/clickhouse/scripts/api_events_v2.sql | 8 ++++++-- crates/analytics/src/api_event/events.rs | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql index b41a75fe67e5..33f158ce48b7 100644 --- a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql +++ b/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql @@ -20,6 +20,7 @@ CREATE TABLE api_events_v2_queue ( `latency` UInt128, `user_agent` String, `ip_addr` String, + `url_path` String ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-api-log-events', kafka_group_name = 'hyper-c1', @@ -50,6 +51,7 @@ CREATE TABLE api_events_v2_dist ( `latency` UInt128, `user_agent` String, `ip_addr` String, + `url_path` String, INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 @@ -82,7 +84,8 @@ CREATE MATERIALIZED VIEW api_events_v2_mv TO api_events_v2_dist ( `inserted_at` DateTime CODEC(T64, LZ4), `latency` UInt128, `user_agent` String, - `ip_addr` String + `ip_addr` String, + `url_path` String ) AS SELECT merchant_id, @@ -106,7 +109,8 @@ SELECT now() as inserted_at, latency, user_agent, - ip_addr + ip_addr, + url_path FROM api_events_v2_queue where length(_error) = 0; diff --git a/crates/analytics/src/api_event/events.rs b/crates/analytics/src/api_event/events.rs index 73b3fb9cbad2..eb9b2d561c53 100644 --- a/crates/analytics/src/api_event/events.rs +++ b/crates/analytics/src/api_event/events.rs @@ -102,4 +102,6 @@ pub struct ApiLogsResult { pub ip_addr: Option, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: PrimitiveDateTime, + pub http_method: Option, + pub url_path: Option, } From b283b6b662c9f2eabe90473434369d8f7c2369a6 Mon Sep 17 00:00:00 2001 From: Shanks Date: Fri, 8 Dec 2023 17:36:17 +0530 Subject: [PATCH 170/443] fix(router): allow zero amount for payment intent in list payment methods (#3090) --- crates/router/src/core/payment_methods/cards.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 84aef952a531..aaecd86627cc 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2237,7 +2237,7 @@ fn filter_amount_based(payment_method: &RequestPaymentMethodTypes, amount: Optio // (Some(amt), Some(max_amt)) => amt <= max_amt, // (_, _) => true, // }; - min_check && max_check + (min_check && max_check) || amount == Some(0) } fn filter_pm_based_on_allowed_types( @@ -2296,8 +2296,9 @@ fn filter_payment_amount_based( pm: &RequestPaymentMethodTypes, ) -> bool { let amount = payment_intent.amount; - pm.maximum_amount.map_or(true, |amt| amount < amt.into()) - && pm.minimum_amount.map_or(true, |amt| amount > amt.into()) + (pm.maximum_amount.map_or(true, |amt| amount <= amt.into()) + && pm.minimum_amount.map_or(true, |amt| amount >= amt.into())) + || payment_intent.amount == 0 } async fn filter_payment_mandate_based( From b279591057cdba6004c99efc82bb856f0bacd1e0 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:58:42 +0530 Subject: [PATCH 171/443] refactor(payment_methods): make the card_holder_name optional for card details in the payment APIs (#3074) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 2 +- .../stripe/payment_intents/types.rs | 2 +- .../stripe/setup_intents/types.rs | 2 +- .../router/src/connector/aci/transformers.rs | 4 +- .../src/connector/bambora/transformers.rs | 6 +- .../braintree_graphql_transformers.rs | 4 +- .../src/connector/dlocal/transformers.rs | 7 +- .../connector/dummyconnector/transformers.rs | 16 ++-- .../src/connector/forte/transformers.rs | 5 +- .../src/connector/mollie/transformers.rs | 5 +- .../router/src/connector/noon/transformers.rs | 5 +- .../src/connector/nuvei/transformers.rs | 2 +- .../src/connector/opayo/transformers.rs | 4 +- .../src/connector/payeezy/transformers.rs | 5 +- .../src/connector/paypal/transformers.rs | 5 +- .../src/connector/powertranz/transformers.rs | 14 +-- .../src/connector/rapyd/transformers.rs | 7 +- .../src/connector/shift4/transformers.rs | 5 +- .../router/src/connector/stax/transformers.rs | 4 +- .../src/connector/worldline/transformers.rs | 5 +- .../router/src/core/payment_methods/vault.rs | 7 +- crates/router/src/core/payments/helpers.rs | 86 +++++++++++++------ .../payments/operations/payment_confirm.rs | 2 + .../payments/operations/payment_create.rs | 2 + .../payments/operations/payment_update.rs | 2 + crates/router/src/types/api/payments.rs | 2 +- crates/router/src/utils/verify_connector.rs | 2 +- crates/router/tests/connectors/aci.rs | 4 +- crates/router/tests/connectors/adyen.rs | 2 +- crates/router/tests/connectors/airwallex.rs | 2 +- .../tests/connectors/authorizedotnet.rs | 2 +- crates/router/tests/connectors/bluesnap.rs | 6 +- crates/router/tests/connectors/fiserv.rs | 2 +- crates/router/tests/connectors/payme.rs | 2 +- crates/router/tests/connectors/rapyd.rs | 4 +- crates/router/tests/connectors/utils.rs | 2 +- crates/router/tests/connectors/worldline.rs | 2 +- crates/router/tests/payments.rs | 4 +- crates/router/tests/payments2.rs | 4 +- 39 files changed, 170 insertions(+), 78 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 93c97cbd443c..b19f4d7b7db1 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -682,7 +682,7 @@ pub struct Card { /// The card holder's name #[schema(value_type = String, example = "John Test")] - pub card_holder_name: Secret, + pub card_holder_name: Option>, /// The CVC number for the card #[schema(value_type = String, example = "242")] diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 3c7d5f2918f1..38007a3110d6 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -118,7 +118,7 @@ impl From for payments::Card { card_number: card.number, card_exp_month: card.exp_month, card_exp_year: card.exp_year, - card_holder_name: card.holder_name.unwrap_or("name".to_string().into()), + card_holder_name: card.holder_name, card_cvc: card.cvc, card_issuer: None, card_network: None, diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 9d3f74af8cb8..4c99d0cb00b4 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -95,7 +95,7 @@ impl From for payments::Card { card_number: card.number, card_exp_month: card.exp_month, card_exp_year: card.exp_year, - card_holder_name: masking::Secret::new("stripe_cust".to_owned()), + card_holder_name: Some(masking::Secret::new("stripe_cust".to_owned())), card_cvc: card.cvc, card_issuer: None, card_network: None, diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 9cfb657bdca8..53639f268c86 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -254,7 +254,9 @@ impl TryFrom for PaymentDetails { fn try_from(card_data: api_models::payments::Card) -> Result { Ok(Self::AciCard(Box::new(CardDetails { card_number: card_data.card_number, - card_holder: card_data.card_holder_name, + card_holder: card_data + .card_holder_name + .ok_or_else(utils::missing_field_err("card_holder_name"))?, card_expiry_month: card_data.card_exp_month, card_expiry_year: card_data.card_exp_year, card_cvv: card_data.card_cvc, diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index 2d50569f9a49..4729bfa5a6ef 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -5,7 +5,7 @@ use masking::{PeekInterface, Secret}; use serde::{Deserialize, Deserializer, Serialize}; use crate::{ - connector::utils::{BrowserInformationData, PaymentsAuthorizeRequestData}, + connector::utils::{self, BrowserInformationData, PaymentsAuthorizeRequestData}, consts, core::errors, services, @@ -117,7 +117,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest { enums::AuthenticationType::NoThreeDs => None, }; let bambora_card = BamboraCard { - name: req_card.card_holder_name, + name: req_card + .card_holder_name + .ok_or_else(utils::missing_field_err("card_holder_name"))?, number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index f6c1bfc46b01..d1201309637a 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -867,7 +867,9 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { expiration_year: card_data.card_exp_year, expiration_month: card_data.card_exp_month, cvv: card_data.card_cvc, - cardholder_name: card_data.card_holder_name, + cardholder_name: card_data + .card_holder_name + .ok_or_else(utils::missing_field_err("card_holder_name"))?, }, }; Ok(Self { diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 92d01cfe56d4..25462c758f17 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils::{AddressDetailsData, PaymentsAuthorizeRequestData, RouterData}, + connector::utils::{self, AddressDetailsData, PaymentsAuthorizeRequestData, RouterData}, core::errors, services, types::{self, api, storage::enums}, @@ -125,7 +125,10 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP document: get_doc_from_currency(country.to_string()), }, card: Some(Card { - holder_name: ccard.card_holder_name.clone(), + holder_name: ccard + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, number: ccard.card_number.clone(), cvv: ccard.card_cvc.clone(), expiration_month: ccard.card_exp_month.clone(), diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs index 3c7bd2e09d9a..bbb3b10c8e00 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ + connector::utils, core::errors, services, types::{self, api, storage::enums}, @@ -83,15 +84,18 @@ pub struct DummyConnectorCard { cvc: Secret, } -impl From for DummyConnectorCard { - fn from(value: api_models::payments::Card) -> Self { - Self { - name: value.card_holder_name, +impl TryFrom for DummyConnectorCard { + type Error = error_stack::Report; + fn try_from(value: api_models::payments::Card) -> Result { + Ok(Self { + name: value + .card_holder_name + .ok_or_else(utils::missing_field_err("card_holder_name"))?, number: value.card_number, expiry_month: value.card_exp_month, expiry_year: value.card_exp_year, cvc: value.card_cvc, - } + }) } } @@ -151,7 +155,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> .payment_method_data { api::PaymentMethodData::Card(ref req_card) => { - Ok(PaymentMethodData::Card(req_card.clone().into())) + Ok(PaymentMethodData::Card(req_card.clone().try_into()?)) } api::PaymentMethodData::Wallet(ref wallet_data) => { Ok(PaymentMethodData::Wallet(wallet_data.clone().try_into()?)) diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index 4bb354f6cda7..411457fab671 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -82,7 +82,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { let address = item.get_billing_address()?; let card = Card { card_type, - name_on_card: ccard.card_holder_name.clone(), + name_on_card: ccard + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, account_number: ccard.card_number.clone(), expire_month: ccard.card_exp_month.clone(), expire_year: ccard.card_exp_year.clone(), diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index 62fb94e236a8..c1151adcf6db 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -286,7 +286,10 @@ impl TryFrom<&types::TokenizationRouterData> for MollieCardTokenRequest { match item.request.payment_method_data.clone() { api_models::payments::PaymentMethodData::Card(ccard) => { let auth = MollieAuthType::try_from(&item.connector_auth_type)?; - let card_holder = ccard.card_holder_name.clone(); + let card_holder = ccard + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?; let card_number = ccard.card_number.clone(); let card_expiry_date = ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()); diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index b478d63e0f12..e2262e7b8959 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -200,7 +200,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { _ => ( match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => Ok(NoonPaymentData::Card(NoonCard { - name_on_card: req_card.card_holder_name.clone(), + name_on_card: req_card + .card_holder_name + .clone() + .ok_or_else(conn_utils::missing_field_err("card_holder_name"))?, number_plain: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), expiry_year: req_card.get_expiry_year_4_digit(), diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index 73e039c63395..4ed6b25b136c 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -1000,7 +1000,7 @@ impl From for PaymentOption { Self { card: Some(Card { card_number: Some(card.card_number), - card_holder_name: Some(card.card_holder_name), + card_holder_name: card.card_holder_name, expiration_month: Some(card.card_exp_month), expiration_year: Some(card.card_exp_year), three_d: card_details.three_d, diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 7b633f6aa641..a0e3877f82b7 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -29,7 +29,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => { let card = OpayoCard { - name: req_card.card_holder_name, + name: req_card + .card_holder_name + .ok_or_else(utils::missing_field_err("card_holder_name"))?, number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 0170d18ecb46..8b4f4a469596 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -241,7 +241,10 @@ fn get_payment_method_data( let card_type = PayeezyCardType::try_from(card.get_card_issuer()?)?; let payeezy_card = PayeezyCard { card_type, - cardholder_name: card.card_holder_name.clone(), + cardholder_name: card + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, card_number: card.card_number.clone(), exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), cvv: card.card_cvc.clone(), diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 8b6a2297d090..0871bc5097af 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -443,7 +443,10 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP let payment_source = Some(PaymentSourceItem::Card(CardRequest { billing_address: get_address_info(item.router_data.address.billing.as_ref())?, expiry, - name: ccard.card_holder_name.clone(), + name: ccard + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, number: Some(ccard.card_number.clone()), security_code: Some(ccard.card_cvc.clone()), attributes, diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index e0ecd81c7e58..6d5a756c571b 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -101,7 +101,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PowertranzPaymentsRequest type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { let source = match item.request.payment_method_data.clone() { - api::PaymentMethodData::Card(card) => Ok(Source::from(&card)), + api::PaymentMethodData::Card(card) => Source::try_from(&card), api::PaymentMethodData::Wallet(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::PayLater(_) @@ -211,15 +211,19 @@ impl TryFrom<&types::BrowserInformation> for BrowserInfo { }) }*/ -impl From<&Card> for Source { - fn from(card: &Card) -> Self { +impl TryFrom<&Card> for Source { + type Error = error_stack::Report; + fn try_from(card: &Card) -> Result { let card = PowertranzCard { - cardholder_name: card.card_holder_name.clone(), + cardholder_name: card + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, card_pan: card.card_number.clone(), card_expiration: card.get_expiry_date_as_yymm(), card_cvv: card.card_cvc.clone(), }; - Self::Card(card) + Ok(Self::Card(card)) } } diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 193eb8198926..aab47bc8b210 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -4,7 +4,7 @@ use time::PrimitiveDateTime; use url::Url; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{self, PaymentsAuthorizeRequestData}, consts, core::errors, pii::Secret, @@ -131,7 +131,10 @@ impl TryFrom<&RapydRouterData<&types::PaymentsAuthorizeRouterData>> for RapydPay number: ccard.card_number.to_owned(), expiration_month: ccard.card_exp_month.to_owned(), expiration_year: ccard.card_exp_year.to_owned(), - name: ccard.card_holder_name.to_owned(), + name: ccard + .card_holder_name + .to_owned() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, cvv: ccard.card_cvc.to_owned(), }), address: None, diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index ce68aad25c50..2b89e7ebf6c0 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -296,7 +296,10 @@ impl number: card.card_number.clone(), exp_month: card.card_exp_month.clone(), exp_year: card.card_exp_year.clone(), - cardholder_name: card.card_holder_name.clone(), + cardholder_name: card + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, }; if item.is_three_ds() { Ok(Self::Cards3DSRequest(Box::new(Cards3DSRequest { diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 2fd3b3474ea4..7395172239e8 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -226,7 +226,9 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { let stax_card_data = StaxTokenizeData { card_exp: card_data .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), - person_name: card_data.card_holder_name, + person_name: card_data + .card_holder_name + .ok_or_else(missing_field_err("card_holder_name"))?, card_number: card_data.card_number, card_cvv: card_data.card_cvc, customer_id: Secret::new(customer_id), diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index b5739fe857ab..d9e9d1ff7c09 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -351,7 +351,10 @@ fn make_card_request( let expiry_date: Secret = Secret::new(secret_value); let card = Card { card_number: ccard.card_number.clone(), - cardholder_name: ccard.card_holder_name.clone(), + cardholder_name: ccard + .card_holder_name + .clone() + .ok_or_else(utils::missing_field_err("card_holder_name"))?, cvv: ccard.card_cvc.clone(), expiry_date, }; diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 5ad78c9d730e..c71632c9b06d 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -51,7 +51,10 @@ impl Vaultable for api::Card { card_number: self.card_number.peek().clone(), exp_year: self.card_exp_year.peek().clone(), exp_month: self.card_exp_month.peek().clone(), - name_on_card: Some(self.card_holder_name.peek().clone()), + name_on_card: self + .card_holder_name + .clone() + .map(|name| name.peek().clone()), nickname: None, card_last_four: None, card_token: None, @@ -99,7 +102,7 @@ impl Vaultable for api::Card { .attach_printable("Invalid card number format from the mock locker")?, card_exp_month: value1.exp_month.into(), card_exp_year: value1.exp_year.into(), - card_holder_name: value1.name_on_card.unwrap_or_default().into(), + card_holder_name: value1.name_on_card.map(masking::Secret::new), card_cvc: value2.card_security_code.unwrap_or_default().into(), card_issuer: None, card_network: None, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4e491964e96c..866a0581e4e9 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -946,6 +946,42 @@ pub fn verify_mandate_details( ) } +// This function validates card_holder_name field to be either null or a non-empty string +pub fn validate_card_holder_name( + payment_method_data: Option, +) -> CustomResult<(), errors::ApiErrorResponse> { + if let Some(pmd) = payment_method_data { + match pmd { + // This validation would occur during payments create + api::PaymentMethodData::Card(card) => { + if let Some(name) = &card.card_holder_name { + if name.clone().expose().is_empty() { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "card_holder_name cannot be empty".to_string(), + }) + .into_report(); + } + } + } + + // This validation would occur during payments confirm + api::PaymentMethodData::CardToken(card) => { + if let Some(name) = card.card_holder_name { + if name.expose().is_empty() { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "card_holder_name cannot be empty".to_string(), + }) + .into_report(); + } + } + } + _ => (), + } + } + + Ok(()) +} + #[instrument(skip_all)] pub fn payment_attempt_status_fsm( payment_method_data: &Option, @@ -1033,7 +1069,7 @@ pub(crate) async fn get_payment_method_create_request( card_number: card.card_number.clone(), card_exp_month: card.card_exp_month.clone(), card_exp_year: card.card_exp_year.clone(), - card_holder_name: Some(card.card_holder_name.clone()), + card_holder_name: card.card_holder_name.clone(), nick_name: card.nick_name.clone(), }; let customer_id = customer.customer_id.clone(); @@ -1404,21 +1440,25 @@ pub async fn retrieve_payment_method_with_temporary_token( let mut updated_card = card.clone(); let mut is_card_updated = false; - let name_on_card = if card.card_holder_name.clone().expose().is_empty() { - card_token_data - .and_then(|token_data| token_data.card_holder_name.clone()) - .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) - .map(|name_on_card| { + // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked + // from payment_method.card_token object + let name_on_card = if let Some(name) = card.card_holder_name.clone() { + if name.expose().is_empty() { + card_token_data.and_then(|token_data| { is_card_updated = true; - name_on_card + token_data.card_holder_name.clone() }) + } else { + card.card_holder_name.clone() + } } else { - Some(card.card_holder_name.clone()) + card_token_data.and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) }; - if let Some(name_on_card) = name_on_card { - updated_card.card_holder_name = name_on_card; - } + updated_card.card_holder_name = name_on_card; if let Some(token_data) = card_token_data { if let Some(cvc) = token_data.card_cvc.clone() { @@ -1487,23 +1527,21 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker")?; - let name_on_card = if let Some(name_on_card) = card.name_on_card.clone() { - if card.name_on_card.unwrap_or_default().expose().is_empty() { - card_token_data - .and_then(|token_data| token_data.card_holder_name.clone()) - .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked + // from payment_method.card_token object + let name_on_card = if let Some(name) = card.name_on_card.clone() { + if name.expose().is_empty() { + card_token_data.and_then(|token_data| token_data.card_holder_name.clone()) } else { - Some(name_on_card) + card.name_on_card } } else { - card_token_data - .and_then(|token_data| token_data.card_holder_name.clone()) - .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + card_token_data.and_then(|token_data| token_data.card_holder_name.clone()) }; let api_card = api::Card { card_number: card.card_number, - card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), + card_holder_name: name_on_card, card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_cvc: card_token_data @@ -3324,7 +3362,7 @@ pub async fn get_additional_payment_data( bank_code: card_data.bank_code.to_owned(), card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: Some(card_data.card_holder_name.clone()), + card_holder_name: card_data.card_holder_name.clone(), last4: last4.clone(), card_isin: card_isin.clone(), }, @@ -3352,7 +3390,7 @@ pub async fn get_additional_payment_data( card_isin: card_isin.clone(), card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: Some(card_data.card_holder_name.clone()), + card_holder_name: card_data.card_holder_name.clone(), }, )) }); @@ -3367,7 +3405,7 @@ pub async fn get_additional_payment_data( card_isin, card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: Some(card_data.card_holder_name.clone()), + card_holder_name: card_data.card_holder_name.clone(), }, ))) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index af2a9fa49c8b..612ddadc1c59 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -871,6 +871,8 @@ impl ValidateRequest ValidateRequest ValidateRequest types::PaymentsAuthorizeRouterData { card_number: cards::CardNumber::from_str("4200000000000000").unwrap(), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("999".to_string()), card_issuer: None, card_network: None, @@ -232,7 +232,7 @@ async fn payments_create_failure() { card_number: cards::CardNumber::from_str("4200000000000000").unwrap(), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("99".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 714dc0d7d672..14177e6fb500 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -126,7 +126,7 @@ impl AdyenTest { card_number: cards::CardNumber::from_str(card_number).unwrap(), card_exp_month: Secret::new(card_exp_month.to_string()), card_exp_year: Secret::new(card_exp_year.to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new(card_cvc.to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/airwallex.rs b/crates/router/tests/connectors/airwallex.rs index 6e7f6c000d28..cfc4c0c003d2 100644 --- a/crates/router/tests/connectors/airwallex.rs +++ b/crates/router/tests/connectors/airwallex.rs @@ -60,7 +60,7 @@ fn payment_method_details() -> Option { card_number: cards::CardNumber::from_str("4035501000000008").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/authorizedotnet.rs b/crates/router/tests/connectors/authorizedotnet.rs index 4021d57d543f..3ae4298e8360 100644 --- a/crates/router/tests/connectors/authorizedotnet.rs +++ b/crates/router/tests/connectors/authorizedotnet.rs @@ -42,7 +42,7 @@ fn get_payment_method_data() -> api::Card { card_number: cards::CardNumber::from_str("5424000000000015").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), ..Default::default() } diff --git a/crates/router/tests/connectors/bluesnap.rs b/crates/router/tests/connectors/bluesnap.rs index 30052d11da45..852b23f022c3 100644 --- a/crates/router/tests/connectors/bluesnap.rs +++ b/crates/router/tests/connectors/bluesnap.rs @@ -400,7 +400,7 @@ async fn should_fail_payment_for_incorrect_cvc() { Some(types::PaymentsAuthorizeData { email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("12345".to_string()), ..utils::CCardType::default().0 }), @@ -426,7 +426,7 @@ async fn should_fail_payment_for_invalid_exp_month() { Some(types::PaymentsAuthorizeData { email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_exp_month: Secret::new("20".to_string()), ..utils::CCardType::default().0 }), @@ -452,7 +452,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { Some(types::PaymentsAuthorizeData { email: Some(Email::from_str("test@gmail.com").unwrap()), payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_exp_year: Secret::new("2000".to_string()), ..utils::CCardType::default().0 }), diff --git a/crates/router/tests/connectors/fiserv.rs b/crates/router/tests/connectors/fiserv.rs index 1394667718c5..36d5f66dbccd 100644 --- a/crates/router/tests/connectors/fiserv.rs +++ b/crates/router/tests/connectors/fiserv.rs @@ -46,7 +46,7 @@ fn payment_method_details() -> Option { card_number: cards::CardNumber::from_str("4005550000000019").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2035".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 5550ba12af88..7de81a8bed28 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -87,7 +87,7 @@ fn payment_method_details() -> Option { card_cvc: Secret::new("123".to_string()), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), ..utils::CCardType::default().0 }), amount: 1000, diff --git a/crates/router/tests/connectors/rapyd.rs b/crates/router/tests/connectors/rapyd.rs index 143f87fc5753..c53f4e8d8b1f 100644 --- a/crates/router/tests/connectors/rapyd.rs +++ b/crates/router/tests/connectors/rapyd.rs @@ -46,7 +46,7 @@ async fn should_only_authorize_payment() { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2024".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, @@ -74,7 +74,7 @@ async fn should_authorize_and_capture_payment() { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), card_exp_month: Secret::new("02".to_string()), card_exp_year: Secret::new("2024".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("123".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 7e5cfeb43974..d3b20b01e4ce 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -869,7 +869,7 @@ impl Default for CCardType { card_number: cards::CardNumber::from_str("4200000000000000").unwrap(), card_exp_month: Secret::new("10".to_string()), card_exp_year: Secret::new("2025".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new("999".to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index fd697f95b754..6a92e0dc93f6 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -71,7 +71,7 @@ impl WorldlineTest { card_number: cards::CardNumber::from_str(card_number).unwrap(), card_exp_month: Secret::new(card_exp_month.to_string()), card_exp_year: Secret::new(card_exp_year.to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(masking::Secret::new("John Doe".to_string())), card_cvc: Secret::new(card_cvc.to_string()), card_issuer: None, card_network: None, diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 9d48aaddd451..8f5ac25736cc 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -320,7 +320,7 @@ async fn payments_create_core() { card_number: "4242424242424242".to_string().try_into().unwrap(), card_exp_month: "10".to_string().into(), card_exp_year: "35".to_string().into(), - card_holder_name: "Arun Raj".to_string().into(), + card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), card_cvc: "123".to_string().into(), card_issuer: None, card_network: None, @@ -496,7 +496,7 @@ async fn payments_create_core_adyen_no_redirect() { card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), card_exp_month: "03".to_string().into(), card_exp_year: "2030".to_string().into(), - card_holder_name: "JohnDoe".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), card_cvc: "737".to_string().into(), card_issuer: None, card_network: None, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 42e5524a15d5..89ac522d237a 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -80,7 +80,7 @@ async fn payments_create_core() { card_number: "4242424242424242".to_string().try_into().unwrap(), card_exp_month: "10".to_string().into(), card_exp_year: "35".to_string().into(), - card_holder_name: "Arun Raj".to_string().into(), + card_holder_name: Some(masking::Secret::new("Arun Raj".to_string())), card_cvc: "123".to_string().into(), card_issuer: None, card_network: None, @@ -263,7 +263,7 @@ async fn payments_create_core_adyen_no_redirect() { card_number: "5555 3412 4444 1115".to_string().try_into().unwrap(), card_exp_month: "03".to_string().into(), card_exp_year: "2030".to_string().into(), - card_holder_name: "JohnDoe".to_string().into(), + card_holder_name: Some(masking::Secret::new("JohnDoe".to_string())), card_cvc: "737".to_string().into(), bank_code: None, card_issuer: None, From 6d5c25e3369117acaf5865965769649d524226af Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 8 Dec 2023 18:00:41 +0530 Subject: [PATCH 172/443] feat(users): Add resend verification email API (#3093) --- crates/api_models/src/events/user.rs | 7 +-- crates/api_models/src/user.rs | 5 ++ crates/router/src/core/errors/user.rs | 5 ++ crates/router/src/core/user.rs | 75 ++++++++++++++++++++------ crates/router/src/routes/app.rs | 4 ++ crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/user.rs | 21 +++++++- crates/router_env/src/logger/types.rs | 2 + 8 files changed, 100 insertions(+), 22 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 92b675723964..1f4cb7359c79 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -8,8 +8,8 @@ use crate::user::{ }, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, - InviteUserResponse, ResetPasswordRequest, SignUpRequest, SignUpWithMerchantIdRequest, - SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, + InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest, + SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, }; impl ApiEventMetric for DashboardEntryResponse { @@ -39,7 +39,8 @@ common_utils::impl_misc_api_event_type!( ResetPasswordRequest, InviteUserRequest, InviteUserResponse, - VerifyEmailRequest + VerifyEmailRequest, + SendVerifyEmailRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 10d8411f8e70..d666dfc3bfa4 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -128,3 +128,8 @@ pub struct VerifyEmailRequest { } pub type VerifyEmailResponse = DashboardEntryResponse; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct SendVerifyEmailRequest { + pub email: pii::Email, +} diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 03ca88056101..330e02cd5471 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -28,6 +28,8 @@ pub enum UserErrors { NameParsingError, #[error("PasswordParsingError")] PasswordParsingError, + #[error("UserAlreadyVerified")] + UserAlreadyVerified, #[error("CompanyNameParsingError")] CompanyNameParsingError, #[error("MerchantAccountCreationError: {0}")] @@ -104,6 +106,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 9, "Invalid Password", None)) } + Self::UserAlreadyVerified => { + AER::Unauthorized(ApiError::new(sub_code, 11, "User already verified", None)) + } Self::CompanyNameParsingError => { AER::BadRequest(ApiError::new(sub_code, 14, "Invalid Company Name", None)) } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index e2c538ca80ee..a9f46a3885f8 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -14,7 +14,6 @@ use super::errors::{UserErrors, UserResponse}; use crate::services::email::types as email_types; use crate::{ consts, - db::user::UserInterface, routes::AppState, services::{authentication as auth, ApplicationResponse}, types::domain, @@ -228,11 +227,12 @@ pub async fn change_password( request: user_api::ChangePasswordRequest, user_from_token: auth::UserFromToken, ) -> UserResponse<()> { - let user: domain::UserFromStorage = - UserInterface::find_user_by_id(&*state.store, &user_from_token.user_id) - .await - .change_context(UserErrors::InternalServerError)? - .into(); + let user: domain::UserFromStorage = state + .store + .find_user_by_id(&user_from_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); user.compare_password(request.old_password.to_owned()) .change_context(UserErrors::InvalidOldPassword)?; @@ -245,17 +245,18 @@ pub async fn change_password( let new_password_hash = utils::user::password::generate_password_hash(new_password.get_secret())?; - let _ = UserInterface::update_user_by_user_id( - &*state.store, - user.get_user_id(), - diesel_models::user::UserUpdate::AccountUpdate { - name: None, - password: Some(new_password_hash), - is_verified: None, - }, - ) - .await - .change_context(UserErrors::InternalServerError)?; + let _ = state + .store + .update_user_by_user_id( + user.get_user_id(), + diesel_models::user::UserUpdate::AccountUpdate { + name: None, + password: Some(new_password_hash), + is_verified: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::StatusOk) } @@ -670,3 +671,43 @@ pub async fn verify_email( utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, )) } + +#[cfg(feature = "email")] +pub async fn send_verification_mail( + state: AppState, + req: user_api::SendVerifyEmailRequest, +) -> UserResponse<()> { + let user_email = domain::UserEmail::try_from(req.email)?; + let user = state + .store + .find_user_by_email(user_email.clone().get_secret().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + if user.is_verified { + return Err(UserErrors::UserAlreadyVerified.into()); + } + + let email_contents = email_types::VerifyEmail { + recipient_email: domain::UserEmail::from_pii_email(user.email)?, + settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", + }; + + state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 34ae6fa3ecb0..eec8a36ce9ab 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -903,6 +903,10 @@ impl User { .route(web::post().to(user_signup_with_merchant_id)), ) .service(web::resource("/verify_email").route(web::post().to(verify_email))) + .service( + web::resource("/verify_email_request") + .route(web::post().to(verify_email_request)), + ); } #[cfg(not(feature = "email"))] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index f5519b960375..10f408f3d4f0 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -171,7 +171,8 @@ impl From for ApiIdentifier { | Flow::ResetPassword | Flow::InviteUser | Flow::UserSignUpWithMerchantId - | Flow::VerifyEmail => Self::User, + | Flow::VerifyEmail + | Flow::VerifyEmailRequest => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 594da67aa023..7f0f0db3b69e 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -33,7 +33,7 @@ pub async fn user_signup_with_merchant_id( &http_req, req_payload.clone(), |state, _, req_body| user_core::signup_with_merchant_id(state, req_body), - &auth::NoAuth, + &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, )) .await @@ -370,3 +370,22 @@ pub async fn verify_email( )) .await } + +#[cfg(feature = "email")] +pub async fn verify_email_request( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmailRequest; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _, req_body| user_core::send_verification_mail(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 6344c21f5f89..e37e15443bdb 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -313,6 +313,8 @@ pub enum Flow { SyncOnboardingStatus, /// Verify email Token VerifyEmail, + /// Send verify email + VerifyEmailRequest, } /// From 72955ecc68280773b9c77b4db3d46de95a62f9ed Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 8 Dec 2023 19:46:16 +0530 Subject: [PATCH 173/443] fix(connector): [CYBERSOURCE] Remove Phone Number Field From Address (#3095) --- .../src/connector/cybersource/transformers.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index d3f542d2013a..a4cea13e2184 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ self, AddressDetailsData, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, - PhoneDetailsData, RouterData, + RouterData, }, consts, core::errors, @@ -60,10 +60,8 @@ pub struct CybersourceZeroMandateRequest { impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { type Error = error_stack::Report; fn try_from(item: &types::SetupMandateRouterData) -> Result { - let phone = item.get_billing_phone()?; - let number_with_code = phone.get_number_with_country_code()?; let email = item.request.get_email()?; - let bill_to = build_bill_to(item.get_billing()?, email, number_with_code)?; + let bill_to = build_bill_to(item.get_billing()?, email)?; let order_information = OrderInformationWithBill { amount_details: Amount { @@ -276,14 +274,12 @@ pub struct BillTo { postal_code: Secret, country: api_enums::CountryAlpha2, email: pii::Email, - phone_number: Secret, } // for cybersource each item in Billing is mandatory fn build_bill_to( address_details: &payments::Address, email: pii::Email, - phone_number: Secret, ) -> Result> { let address = address_details .address @@ -298,7 +294,6 @@ fn build_bill_to( postal_code: address.get_zip()?.to_owned(), country: address.get_country()?.to_owned(), email, - phone_number, }) } @@ -309,10 +304,8 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - let phone = item.router_data.get_billing_phone()?; - let number_with_code = phone.get_number_with_country_code()?; let email = item.router_data.request.get_email()?; - let bill_to = build_bill_to(item.router_data.get_billing()?, email, number_with_code)?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; let order_information = OrderInformationWithBill { amount_details: Amount { From 4e8de46423829a46c787ed9b7e015ce2680692be Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 07:54:25 +0000 Subject: [PATCH 174/443] chore(version): v1.98.0 --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfcc08d08ef..62ed03591eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.98.0 (2023-12-11) + +### Features + +- **connector:** Accept connector_transaction_id in error_response of connector flows for Trustpay ([#3060](https://github.com/juspay/hyperswitch/pull/3060)) ([`f53b090`](https://github.com/juspay/hyperswitch/commit/f53b090db87e094f9694481f13af62240c4c422a)) +- **pm_auth:** Pm_auth service migration ([#3047](https://github.com/juspay/hyperswitch/pull/3047)) ([`9c1c44a`](https://github.com/juspay/hyperswitch/commit/9c1c44a706750b14857e9180f5161b61ed89a2ad)) +- **user:** Add `verify_email` API ([#3076](https://github.com/juspay/hyperswitch/pull/3076)) ([`585e009`](https://github.com/juspay/hyperswitch/commit/585e00980c43797f326efb809df9ffd497d1dd26)) +- **users:** Add resend verification email API ([#3093](https://github.com/juspay/hyperswitch/pull/3093)) ([`6d5c25e`](https://github.com/juspay/hyperswitch/commit/6d5c25e3369117acaf5865965769649d524226af)) + +### Bug Fixes + +- **analytics:** Adding api_path to api logs event and to auditlogs api response ([#3079](https://github.com/juspay/hyperswitch/pull/3079)) ([`bf67438`](https://github.com/juspay/hyperswitch/commit/bf674380d5c7e856d0bae75554326aa9017c0201)) +- **config:** Add missing config fields in `docker_compose.toml` ([#3080](https://github.com/juspay/hyperswitch/pull/3080)) ([`1f8116d`](https://github.com/juspay/hyperswitch/commit/1f8116db368aec344d08603045c4cb46c2c25b41)) +- **connector:** [CYBERSOURCE] Remove Phone Number Field From Address ([#3095](https://github.com/juspay/hyperswitch/pull/3095)) ([`72955ec`](https://github.com/juspay/hyperswitch/commit/72955ecc68280773b9c77b4db3d46de95a62f9ed)) +- **drainer:** Properly log deserialization errors ([#3075](https://github.com/juspay/hyperswitch/pull/3075)) ([`42b5bd4`](https://github.com/juspay/hyperswitch/commit/42b5bd4f3d142c9fa12475f36a8b144753ac06e2)) +- **router:** Allow zero amount for payment intent in list payment methods ([#3090](https://github.com/juspay/hyperswitch/pull/3090)) ([`b283b6b`](https://github.com/juspay/hyperswitch/commit/b283b6b662c9f2eabe90473434369d8f7c2369a6)) +- **user:** Add checks for change password ([#3078](https://github.com/juspay/hyperswitch/pull/3078)) ([`26a2611`](https://github.com/juspay/hyperswitch/commit/26a261131b4dbb8570e139127a2c0d356e2820be)) + +### Refactors + +- **payment_methods:** Make the card_holder_name optional for card details in the payment APIs ([#3074](https://github.com/juspay/hyperswitch/pull/3074)) ([`b279591`](https://github.com/juspay/hyperswitch/commit/b279591057cdba6004c99efc82bb856f0bacd1e0)) +- **user:** Add account verification check in signin ([#3082](https://github.com/juspay/hyperswitch/pull/3082)) ([`f7d6e3c`](https://github.com/juspay/hyperswitch/commit/f7d6e3c0149869175a59996e67d3e2d3b6f3b8c2)) + +### Documentation + +- **openapi:** Fix `payment_methods_enabled` OpenAPI spec in merchant connector account APIs ([#3068](https://github.com/juspay/hyperswitch/pull/3068)) ([`b6838c4`](https://github.com/juspay/hyperswitch/commit/b6838c4d1a3a456e28a5f438fcd74a60bedb2539)) + +### Miscellaneous Tasks + +- **configs:** [CYBERSOURCE] Add mandate configs ([#3085](https://github.com/juspay/hyperswitch/pull/3085)) ([`777cd5c`](https://github.com/juspay/hyperswitch/commit/777cd5cdc2342fb7195a06505647fa331725e1dd)) + +**Full Changelog:** [`v1.97.0...v1.98.0`](https://github.com/juspay/hyperswitch/compare/v1.97.0...v1.98.0) + +- - - + + ## 1.97.0 (2023-12-06) ### Features From 2c4599a1cd7e244b6fb11948c88c55c5b8faad76 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:20:19 +0530 Subject: [PATCH 175/443] feat: add utility to convert TOML configuration file to list of environment variables (#3096) --- .github/workflows/CI-pr.yml | 386 ++++++-------------------- .github/workflows/CI-push.yml | 6 +- Cargo.lock | 281 ++++++++++--------- Makefile | 2 +- crates/analytics/Cargo.toml | 4 +- crates/api_models/Cargo.toml | 4 +- crates/cards/Cargo.toml | 4 +- crates/common_enums/Cargo.toml | 6 +- crates/common_utils/Cargo.toml | 4 +- crates/config_importer/Cargo.toml | 24 ++ crates/config_importer/README.md | 41 +++ crates/config_importer/src/cli.rs | 43 +++ crates/config_importer/src/main.rs | 98 +++++++ crates/currency_conversion/Cargo.toml | 2 +- crates/data_models/Cargo.toml | 4 +- crates/diesel_models/Cargo.toml | 4 +- crates/drainer/Cargo.toml | 6 +- crates/euclid/Cargo.toml | 4 +- crates/external_services/Cargo.toml | 2 +- crates/kgraph_utils/Cargo.toml | 4 +- crates/masking/Cargo.toml | 4 +- crates/masking/tests/basic.rs | 4 +- crates/pm_auth/Cargo.toml | 4 +- crates/redis_interface/Cargo.toml | 2 +- crates/router/Cargo.toml | 10 +- crates/router_derive/Cargo.toml | 6 +- crates/router_env/Cargo.toml | 6 +- crates/scheduler/Cargo.toml | 4 +- crates/storage_impl/Cargo.toml | 4 +- crates/test_utils/Cargo.toml | 4 +- 30 files changed, 498 insertions(+), 479 deletions(-) create mode 100644 crates/config_importer/Cargo.toml create mode 100644 crates/config_importer/README.md create mode 100644 crates/config_importer/src/cli.rs create mode 100644 crates/config_importer/src/main.rs diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index 79cb352acbb8..d6b3d98b8c82 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -130,159 +130,53 @@ jobs: shell: bash run: sed -i 's/rustflags = \[/rustflags = \[\n "-Dwarnings",/' .cargo/config.toml - - name: Check files changed + - name: Check modified crates shell: bash run: | - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/api_models/; then - echo "api_models_changes_exist=false" >> $GITHUB_ENV - else - echo "api_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/cards/; then - echo "cards_changes_exist=false" >> $GITHUB_ENV - else - echo "cards_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_enums/; then - echo "common_enums_changes_exist=false" >> $GITHUB_ENV - else - echo "common_enums_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_utils/; then - echo "common_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "common_utils_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/diesel_models/; then - echo "diesel_models_changes_exist=false" >> $GITHUB_ENV - else - echo "diesel_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/drainer/; then - echo "drainer_changes_exist=false" >> $GITHUB_ENV - else - echo "drainer_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/external_services/; then - echo "external_services_changes_exist=false" >> $GITHUB_ENV - else - echo "external_services_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/masking/; then - echo "masking_changes_exist=false" >> $GITHUB_ENV - else - echo "masking_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/redis_interface/; then - echo "redis_interface_changes_exist=false" >> $GITHUB_ENV - else - echo "redis_interface_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router/; then - echo "router_changes_exist=false" >> $GITHUB_ENV - else - echo "router_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/storage_impl/; then - echo "storage_impl_changes_exist=false" >> $GITHUB_ENV - else - echo "storage_impl_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_derive/; then - echo "router_derive_changes_exist=false" >> $GITHUB_ENV - else - echo "router_derive_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_env/; then - echo "router_env_changes_exist=false" >> $GITHUB_ENV - else - echo "router_env_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/test_utils/; then - echo "test_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "test_utils_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --submodule=diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/pm_auth/; then - echo "pm_auth_changes_exist=false" >> $GITHUB_ENV - else - echo "pm_auth_changes_exist=true" >> $GITHUB_ENV - fi - - - name: Cargo hack api_models - if: env.api_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p api_models - - - name: Cargo hack cards - if: env.cards_changes_exist == 'true' + # Obtain a list of workspace members + workspace_members="$( + cargo metadata --format-version 1 --no-deps \ + | jq --compact-output --monochrome-output --raw-output '.workspace_members | sort | .[] | split(" ")[0]' + )" + + PACKAGES_CHECKED=() + PACKAGES_SKIPPED=() + + while IFS= read -r package_name; do + # Obtain comma-separated list of transitive workspace dependencies for each workspace member + change_paths="$(cargo tree --all-features --no-dedupe --prefix none --package "${package_name}" \ + | grep 'crates/' \ + | sort --unique \ + | awk --field-separator ' ' '{ printf "crates/%s\n", $1 }' | paste -d ',' -s -)" + + # Store change paths in an array by splitting `change_paths` by comma + IFS=',' read -ra change_paths <<< "${change_paths}" + + # A package must be checked if any of its transitive dependencies (or itself) has been modified + if git diff --exit-code --quiet "origin/${GITHUB_BASE_REF}" -- "${change_paths[@]}"; then + printf '::debug::Skipping `%s` since none of these paths were modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_SKIPPED+=("${package_name}") + else + printf '::debug::Checking `%s` since at least one of these paths was modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_CHECKED+=("${package_name}") + fi + done <<< "${workspace_members}" + + printf '::notice::Packages checked: %s; Packages skipped: %s\n' "${PACKAGES_CHECKED[*]}" "${PACKAGES_SKIPPED[*]}" + echo "PACKAGES_CHECKED=${PACKAGES_CHECKED[*]}" >> ${GITHUB_ENV} + echo "PACKAGES_SKIPPED=${PACKAGES_SKIPPED[*]}" >> ${GITHUB_ENV} + + - name: Run `cargo hack` on modified crates shell: bash - run: cargo hack check --each-feature --no-dev-deps -p cards - - - name: Cargo hack common_enums - if: env.common_enums_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_enums - - - name: Cargo hack common_utils - if: env.common_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_utils - - - name: Cargo hack diesel_models - if: env.diesel_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p diesel_models - - - name: Cargo hack drainer - if: env.drainer_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p drainer - - - name: Cargo hack external_services - if: env.external_services_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p external_services - - - name: Cargo hack masking - if: env.masking_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p masking - - - name: Cargo hack redis_interface - if: env.redis_interface_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p redis_interface - - - name: Cargo hack pm_auth - if: env.pm_auth_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p pm_auth - - - name: Cargo hack router - if: env.router_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --skip kms,basilisk,kv_store,accounts_cache,openapi --no-dev-deps -p router - - - name: Cargo hack storage_impl - if: env.storage_impl_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p storage_impl - - - name: Cargo hack router_derive - if: env.router_derive_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_derive + run: | + # Store packages to check in an array by splitting `PACKAGES_CHECKED` by space + IFS=' ' read -ra PACKAGES_CHECKED <<< "${PACKAGES_CHECKED}" - - name: Cargo hack router_env - if: env.router_env_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_env - - - name: Cargo hack test_utils - if: env.test_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p test_utils + for package in "${PACKAGES_CHECKED[@]}"; do + printf '::group::Running `cargo hack` on package `%s`\n' "${package}" + cargo hack check --each-feature --all-targets --package "${package}" + echo '::endgroup::' + done # cargo-deny: # name: Run cargo-deny @@ -393,159 +287,53 @@ jobs: git push fi - - name: Check files changed + - name: Check modified crates shell: bash run: | - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/api_models/; then - echo "api_models_changes_exist=false" >> $GITHUB_ENV - else - echo "api_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/cards/; then - echo "cards_changes_exist=false" >> $GITHUB_ENV - else - echo "cards_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_enums/; then - echo "common_enums_changes_exist=false" >> $GITHUB_ENV - else - echo "common_enums_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/common_utils/; then - echo "common_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "common_utils_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/diesel_models/; then - echo "diesel_models_changes_exist=false" >> $GITHUB_ENV - else - echo "diesel_models_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/drainer/; then - echo "drainer_changes_exist=false" >> $GITHUB_ENV - else - echo "drainer_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/external_services/; then - echo "external_services_changes_exist=false" >> $GITHUB_ENV - else - echo "external_services_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/masking/; then - echo "masking_changes_exist=false" >> $GITHUB_ENV - else - echo "masking_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/redis_interface/; then - echo "redis_interface_changes_exist=false" >> $GITHUB_ENV - else - echo "redis_interface_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router/; then - echo "router_changes_exist=false" >> $GITHUB_ENV - else - echo "router_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_derive/; then - echo "router_derive_changes_exist=false" >> $GITHUB_ENV - else - echo "router_derive_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/storage_impl/; then - echo "storage_impl_changes_exist=false" >> $GITHUB_ENV - else - echo "storage_impl_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/router_env/; then - echo "router_env_changes_exist=false" >> $GITHUB_ENV - else - echo "router_env_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/test_utils/; then - echo "test_utils_changes_exist=false" >> $GITHUB_ENV - else - echo "test_utils_changes_exist=true" >> $GITHUB_ENV - fi - if git diff --submodule=diff --exit-code --quiet origin/$GITHUB_BASE_REF -- crates/pm_auth/; then - echo "pm_auth_changes_exist=false" >> $GITHUB_ENV - else - echo "pm_auth_changes_exist=true" >> $GITHUB_ENV - fi - - - name: Cargo hack api_models - if: env.api_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p api_models - - - name: Cargo hack cards - if: env.cards_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p cards - - - name: Cargo hack common_enums - if: env.common_enums_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_enums - - - name: Cargo hack common_utils - if: env.common_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p common_utils - - - name: Cargo hack diesel_models - if: env.diesel_models_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p diesel_models - - - name: Cargo hack drainer - if: env.drainer_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p drainer - - - name: Cargo hack external_services - if: env.external_services_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p external_services - - - name: Cargo hack masking - if: env.masking_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p masking - - - name: Cargo hack redis_interface - if: env.redis_interface_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p redis_interface - - - name: Cargo hack pm_auth - if: env.pm_auth_changes_exist == 'true' + # Obtain a list of workspace members + workspace_members="$( + cargo metadata --format-version 1 --no-deps \ + | jq --compact-output --monochrome-output --raw-output '.workspace_members | sort | .[] | split(" ")[0]' + )" + + PACKAGES_CHECKED=() + PACKAGES_SKIPPED=() + + while IFS= read -r package_name; do + # Obtain comma-separated list of transitive workspace dependencies for each workspace member + change_paths="$(cargo tree --all-features --no-dedupe --prefix none --package "${package_name}" \ + | grep 'crates/' \ + | sort --unique \ + | awk --field-separator ' ' '{ printf "crates/%s\n", $1 }' | paste -d ',' -s -)" + + # Store change paths in an array by splitting `change_paths` by comma + IFS=',' read -ra change_paths <<< "${change_paths}" + + # A package must be checked if any of its transitive dependencies (or itself) has been modified + if git diff --exit-code --quiet "origin/${GITHUB_BASE_REF}" -- "${change_paths[@]}"; then + printf '::debug::Skipping `%s` since none of these paths were modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_SKIPPED+=("${package_name}") + else + printf '::debug::Checking `%s` since at least one of these paths was modified: %s\n' "${package_name}" "${change_paths[*]}" + PACKAGES_CHECKED+=("${package_name}") + fi + done <<< "${workspace_members}" + + printf '::notice::Packages checked: %s; Packages skipped: %s\n' "${PACKAGES_CHECKED[*]}" "${PACKAGES_SKIPPED[*]}" + echo "PACKAGES_CHECKED=${PACKAGES_CHECKED[*]}" >> ${GITHUB_ENV} + echo "PACKAGES_SKIPPED=${PACKAGES_SKIPPED[*]}" >> ${GITHUB_ENV} + + - name: Run `cargo hack` on modified crates shell: bash - run: cargo hack check --each-feature --no-dev-deps -p pm_auth - - - name: Cargo hack router - if: env.router_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --skip kms,basilisk,kv_store,accounts_cache,openapi --no-dev-deps -p router - - - name: Cargo hack router_derive - if: env.router_derive_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_derive - - - name: Cargo hack storage_impl - if: env.storage_impl_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p storage_impl - - - name: Cargo hack router_env - if: env.router_env_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p router_env - - - name: Cargo hack test_utils - if: env.test_utils_changes_exist == 'true' - shell: bash - run: cargo hack check --each-feature --no-dev-deps -p test_utils + run: | + # Store packages to check in an array by splitting `PACKAGES_CHECKED` by space + IFS=' ' read -ra PACKAGES_CHECKED <<< "${PACKAGES_CHECKED}" + + for package in "${PACKAGES_CHECKED[@]}"; do + printf '::group::Running `cargo hack` on package `%s`\n' "${package}" + cargo hack check --each-feature --all-targets --package "${package}" + echo '::endgroup::' + done typos: name: Spell check diff --git a/.github/workflows/CI-push.yml b/.github/workflows/CI-push.yml index a6a4bde5a5d4..90b301bbd9e5 100644 --- a/.github/workflows/CI-push.yml +++ b/.github/workflows/CI-push.yml @@ -80,8 +80,8 @@ jobs: - name: Cargo hack if: ${{ github.event_name == 'push' }} shell: bash - run: cargo hack check --each-feature --no-dev-deps - + run: cargo hack check --workspace --each-feature --all-targets + - name: Cargo build release if: ${{ github.event_name == 'merge_group' }} shell: bash @@ -166,7 +166,7 @@ jobs: - name: Cargo hack if: ${{ github.event_name == 'push' }} shell: bash - run: cargo hack check --each-feature --no-dev-deps + run: cargo hack check --workspace --each-feature --all-targets - name: Cargo build release if: ${{ github.event_name == 'merge_group' }} diff --git a/Cargo.lock b/Cargo.lock index 307a5ca2398d..54d37da3a8e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,8 +44,8 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "ahash 0.8.3", - "base64 0.21.4", + "ahash 0.8.6", + "base64 0.21.5", "bitflags 1.3.2", "brotli", "bytes 1.5.0", @@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -118,7 +118,7 @@ dependencies = [ "parse-size", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -220,7 +220,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash 0.7.6", + "ahash 0.7.7", "bytes 1.5.0", "bytestring", "cfg-if 1.0.0", @@ -255,7 +255,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -281,25 +281,26 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if 1.0.0", - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -589,7 +590,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -600,7 +601,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -636,8 +637,8 @@ dependencies = [ "actix-service", "actix-tls", "actix-utils", - "ahash 0.7.6", - "base64 0.21.4", + "ahash 0.7.7", + "base64 0.21.5", "bytes 1.5.0", "cfg-if 1.0.0", "cookie", @@ -1163,9 +1164,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64-simd" @@ -1317,7 +1318,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "syn_derive", ] @@ -1609,9 +1610,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.4" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" dependencies = [ "clap_builder", "clap_derive", @@ -1620,9 +1621,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.4" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ "anstyle", "bitflags 1.3.2", @@ -1638,7 +1639,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1740,6 +1741,18 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "config_importer" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "indexmap 2.1.0", + "serde", + "serde_json", + "toml 0.7.4", +] + [[package]] name = "constant_time_eq" version = "0.2.6" @@ -2012,7 +2025,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2023,7 +2036,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2033,7 +2046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if 1.0.0", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "lock_api 0.4.10", "once_cell", "parking_lot_core 0.9.8", @@ -2160,7 +2173,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2190,7 +2203,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2247,7 +2260,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2401,7 +2414,7 @@ dependencies = [ "common_enums", "currency_conversion", "euclid", - "getrandom 0.2.10", + "getrandom 0.2.11", "kgraph_utils", "once_cell", "ron-parser", @@ -2427,7 +2440,7 @@ dependencies = [ "aws-sdk-sesv2", "aws-sdk-sts", "aws-smithy-client", - "base64 0.21.4", + "base64 0.21.5", "common_utils", "dyn-clone", "error-stack", @@ -2597,7 +2610,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2609,7 +2622,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2621,7 +2634,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2734,7 +2747,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2806,9 +2819,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2907,16 +2920,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", ] [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.6", "allocator-api2", ] @@ -2926,7 +2939,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.3", ] [[package]] @@ -2935,7 +2948,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "bytes 1.5.0", "headers-core", "http", @@ -3140,16 +3153,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -3232,12 +3245,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "serde", ] @@ -3342,7 +3355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33a96c4f2128a6f44ecf7c36df2b03dddf5a07b060a4d5ebc0a81e9821f7c60e" dependencies = [ "anyhow", - "base64 0.21.4", + "base64 0.21.5", "flate2", "once_cell", "openssl", @@ -3388,7 +3401,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "pem", "ring", "serde", @@ -3434,9 +3447,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libgit2-sys" @@ -3909,9 +3922,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -4013,7 +4026,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4299,7 +4312,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4389,7 +4402,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -4509,7 +4522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.10", + "toml_edit 0.19.14", ] [[package]] @@ -4547,9 +4560,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.68" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -4755,7 +4768,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -4893,7 +4906,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "redox_syscall 0.2.16", "thiserror", ] @@ -4970,7 +4983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "async-compression", - "base64 0.21.4", + "base64 0.21.5", "bytes 1.5.0", "encoding_rs", "futures-core", @@ -5094,7 +5107,7 @@ dependencies = [ "awc", "aws-config", "aws-sdk-s3", - "base64 0.21.4", + "base64 0.21.5", "bb8", "bigdecimal", "blake3", @@ -5180,13 +5193,13 @@ name = "router_derive" version = "0.1.0" dependencies = [ "diesel", - "indexmap 2.0.2", + "indexmap 2.1.0", "proc-macro2", "quote", "serde", "serde_json", "strum 0.24.1", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5246,7 +5259,7 @@ dependencies = [ "quote", "rust-embed-utils", "shellexpand", - "syn 2.0.38", + "syn 2.0.39", "walkdir", ] @@ -5404,7 +5417,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", ] [[package]] @@ -5580,9 +5593,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -5600,22 +5613,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -5670,7 +5683,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5696,15 +5709,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", @@ -5713,14 +5726,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5745,7 +5758,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -5968,7 +5981,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", "atoi", "base64 0.13.1", "bigdecimal", @@ -6147,7 +6160,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6169,9 +6182,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -6187,7 +6200,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6295,7 +6308,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6307,7 +6320,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "test-case-core", ] @@ -6316,7 +6329,7 @@ name = "test_utils" version = "0.1.0" dependencies = [ "async-trait", - "base64 0.21.4", + "base64 0.21.5", "clap", "masking", "rand 0.8.5", @@ -6386,7 +6399,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6412,9 +6425,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" dependencies = [ "itoa", "serde", @@ -6430,9 +6443,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" dependencies = [ "time-core", ] @@ -6576,7 +6589,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -6745,10 +6758,11 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ + "indexmap 1.9.3", "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.19.10", + "toml_edit 0.19.14", ] [[package]] @@ -6762,15 +6776,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.4.11", + "winnow", ] [[package]] @@ -6779,9 +6793,9 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "toml_datetime", - "winnow 0.5.19", + "winnow", ] [[package]] @@ -7145,7 +7159,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "utoipa-gen", @@ -7160,7 +7174,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -7186,7 +7200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "atomic", - "getrandom 0.2.10", + "getrandom 0.2.11", "serde", ] @@ -7309,7 +7323,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -7343,7 +7357,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7462,10 +7476,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] @@ -7536,15 +7550,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "winnow" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656953b22bcbfb1ec8179d60734981d1904494ecc91f8a3f0ee5c7389bb8eb4b" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.5.19" @@ -7566,13 +7571,13 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.5.19" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f71803d3a1c80377a06221e0530be02035d5b3e854af56c6ece7ac20ac441d" +checksum = "bd7b0b5b253ebc0240d6aac6dd671c495c467420577bf634d3064ae7e6fa2b4c" dependencies = [ "assert-json-diff", "async-trait", - "base64 0.21.4", + "base64 0.21.5", "deadpool", "futures 0.3.28", "futures-timer", @@ -7637,6 +7642,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Makefile b/Makefile index abe0dd50b145..9b62b3c5c99d 100644 --- a/Makefile +++ b/Makefile @@ -93,4 +93,4 @@ precommit : fmt clippy test hack: - cargo hack check --workspace --each-feature --no-dev-deps \ No newline at end of file + cargo hack check --workspace --each-feature --all-targets \ No newline at end of file diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml index f49fe322ae3b..25066970ddcd 100644 --- a/crates/analytics/Cargo.toml +++ b/crates/analytics/Cargo.toml @@ -28,8 +28,8 @@ error-stack = "0.3.1" futures = "0.3.28" once_cell = "1.18.0" reqwest = { version = "0.11.18", features = ["serde_json"] } -serde = { version = "1.0.163", features = ["derive", "rc"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive", "rc"] } +serde_json = "1.0.108" sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.43" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index afba129b601e..69980361500c 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -24,8 +24,8 @@ actix-web = { version = "4.3.1", optional = true } error-stack = "0.3.1" mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" strum = { version = "0.25", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } diff --git a/crates/cards/Cargo.toml b/crates/cards/Cargo.toml index 00445936e3d2..ae72a3d43acc 100644 --- a/crates/cards/Cargo.toml +++ b/crates/cards/Cargo.toml @@ -12,7 +12,7 @@ license.workspace = true [dependencies] error-stack = "0.3.1" luhn = "1.0.1" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" time = "0.3.21" @@ -21,4 +21,4 @@ common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } [dev-dependencies] -serde_json = "1.0.96" +serde_json = "1.0.108" diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 72d9f6bb0bb1..3ed01ca2a97a 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -12,8 +12,8 @@ dummy_connector = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.160", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" strum = { version = "0.25", features = ["derive"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } @@ -21,4 +21,4 @@ utoipa = { version = "3.3.0", features = ["preserve_order"] } router_derive = { version = "0.1.0", path = "../router_derive" } [dev-dependencies] -serde_json = "1.0.96" +serde_json = "1.0.108" diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3619c93d772c..739129d02db2 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -30,8 +30,8 @@ regex = "1.8.4" reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] } ring = { version = "0.16.20", features = ["std"] } rustc-hash = "1.1.0" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" serde_urlencoded = "0.7.1" signal-hook = { version = "0.3.15", optional = true } strum = { version = "0.24.1", features = ["derive"] } diff --git a/crates/config_importer/Cargo.toml b/crates/config_importer/Cargo.toml new file mode 100644 index 000000000000..d831812cdcab --- /dev/null +++ b/crates/config_importer/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "config_importer" +description = "Utility to convert a TOML configuration file to a list of environment variables" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +readme = "README.md" +license.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +anyhow = "1.0.75" +clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } +indexmap = { version = "2.1.0", optional = true } +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +toml = { version = "0.7.4", default-features = false, features = ["parse"] } + +[features] +default = ["preserve_order"] +preserve_order = ["dep:indexmap", "serde_json/preserve_order", "toml/preserve_order"] diff --git a/crates/config_importer/README.md b/crates/config_importer/README.md new file mode 100644 index 000000000000..a7190e6441b7 --- /dev/null +++ b/crates/config_importer/README.md @@ -0,0 +1,41 @@ +# config_importer + +A simple utility tool to import a Hyperswitch TOML configuration file, convert +it into environment variable key-value pairs, and export it in the specified +format. +As of now, it supports only exporting the environment variables to a JSON format +compatible with Kubernetes, but it can be easily extended to export to a YAML +format compatible with Kubernetes or the env file format. + +## Usage + +You can find the usage information from the help message by specifying the +`--help` flag: + +```shell +cargo run --bin config_importer -- --help +``` + +### Specifying the output location + +If the `--output-file` flag is not specified, the utility prints the output to +stdout. +If you would like to write the output to a file instead, you can specify the +`--output-file` flag with the path to the output file: + +```shell +cargo run --bin config_importer -- --input-file config/development.toml --output-file config/development.json +``` + +### Specifying a different prefix + +If the `--prefix` flag is not specified, the default prefix `ROUTER` is +considered, which generates the environment variables accepted by the `router` +binary/application. +If you'd want to generate environment variables for the `drainer` +binary/application, then you can specify the `--prefix` flag with value +`drainer` (or `DRAINER`, both work). + +```shell +cargo run --bin config_importer -- --input-file config/drainer.toml --prefix drainer +``` diff --git a/crates/config_importer/src/cli.rs b/crates/config_importer/src/cli.rs new file mode 100644 index 000000000000..d394f51b4b3f --- /dev/null +++ b/crates/config_importer/src/cli.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; + +/// Utility to import a hyperswitch TOML configuration file, convert it into environment variable +/// key-value pairs, and export it in the specified format. +#[derive(clap::Parser, Debug)] +#[command(arg_required_else_help = true)] +pub(crate) struct Args { + /// Input TOML configuration file. + #[arg(short, long, value_name = "FILE")] + pub(crate) input_file: PathBuf, + + /// The format to convert the environment variables to. + #[arg( + value_enum, + short = 'f', + long, + value_name = "FORMAT", + default_value = "kubernetes-json" + )] + pub(crate) output_format: OutputFormat, + + /// Output file. Output will be written to stdout if not specified. + #[arg(short, long, value_name = "FILE")] + pub(crate) output_file: Option, + + /// Prefix to be used for each environment variable in the generated output. + #[arg(short, long, default_value = "ROUTER")] + pub(crate) prefix: String, +} + +/// The output format to convert environment variables to. +#[derive(clap::ValueEnum, Clone, Copy, Debug)] +pub(crate) enum OutputFormat { + /// Converts each environment variable to an object containing `name` and `value` fields. + /// + /// ```json + /// { + /// "name": "ENVIRONMENT", + /// "value": "PRODUCTION" + /// } + /// ``` + KubernetesJson, +} diff --git a/crates/config_importer/src/main.rs b/crates/config_importer/src/main.rs new file mode 100644 index 000000000000..2a29ad56dc7e --- /dev/null +++ b/crates/config_importer/src/main.rs @@ -0,0 +1,98 @@ +mod cli; + +use std::io::{BufWriter, Write}; + +use anyhow::Context; + +/// The separator used in environment variable names. +const ENV_VAR_SEPARATOR: &str = "__"; + +#[cfg(not(feature = "preserve_order"))] +type EnvironmentVariableMap = std::collections::HashMap; + +#[cfg(feature = "preserve_order")] +type EnvironmentVariableMap = indexmap::IndexMap; + +fn main() -> anyhow::Result<()> { + let args = ::parse(); + + // Read input TOML file + let toml_contents = + std::fs::read_to_string(args.input_file).context("Failed to read input file")?; + let table = toml_contents + .parse::() + .context("Failed to parse TOML file contents")?; + + // Parse TOML file contents to a `HashMap` of environment variable name and value pairs + let env_vars = table + .iter() + .flat_map(|(key, value)| process_toml_value(&args.prefix, key, value)) + .collect::(); + + let writer: BufWriter> = match args.output_file { + // Write to file if output file is specified + Some(file) => BufWriter::new(Box::new( + std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(file) + .context("Failed to open output file")?, + )), + // Write to stdout otherwise + None => BufWriter::new(Box::new(std::io::stdout().lock())), + }; + + // Write environment variables in specified format + match args.output_format { + cli::OutputFormat::KubernetesJson => { + let k8s_env_vars = env_vars + .into_iter() + .map(|(name, value)| KubernetesEnvironmentVariable { name, value }) + .collect::>(); + serde_json::to_writer_pretty(writer, &k8s_env_vars) + .context("Failed to serialize environment variables as JSON")? + } + } + + Ok(()) +} + +fn process_toml_value( + prefix: impl std::fmt::Display + Clone, + key: impl std::fmt::Display + Clone, + value: &toml::Value, +) -> Vec<(String, String)> { + let key_with_prefix = format!("{prefix}{ENV_VAR_SEPARATOR}{key}").to_ascii_uppercase(); + + match value { + toml::Value::String(s) => vec![(key_with_prefix, s.to_owned())], + toml::Value::Integer(i) => vec![(key_with_prefix, i.to_string())], + toml::Value::Float(f) => vec![(key_with_prefix, f.to_string())], + toml::Value::Boolean(b) => vec![(key_with_prefix, b.to_string())], + toml::Value::Datetime(dt) => vec![(key_with_prefix, dt.to_string())], + toml::Value::Array(values) => { + if values.is_empty() { + return vec![(key_with_prefix, String::new())]; + } + + // This logic does not support / account for arrays of tables or arrays of arrays. + let (_processed_keys, processed_values) = values + .iter() + .flat_map(|v| process_toml_value(prefix.clone(), key.clone(), v)) + .unzip::<_, _, Vec, Vec>(); + vec![(key_with_prefix, processed_values.join(","))] + } + toml::Value::Table(map) => map + .into_iter() + .flat_map(|(k, v)| process_toml_value(key_with_prefix.clone(), k, v)) + .collect(), + } +} + +/// The Kubernetes environment variable structure containing a name and a value. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct KubernetesEnvironmentVariable { + name: String, + value: String, +} diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml index 7eb3af7d526d..d84956fe2f76 100644 --- a/crates/currency_conversion/Cargo.toml +++ b/crates/currency_conversion/Cargo.toml @@ -12,5 +12,5 @@ common_enums = { version = "0.1.0", path = "../common_enums", package = "common_ # Third party crates rust_decimal = "1.29" rusty-money = { version = "0.4.0", features = ["iso", "crypto"] } -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.43" diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index a86dc3070b4d..39bcc71341bc 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -22,7 +22,7 @@ diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_ # Third party deps async-trait = "0.1.68" error-stack = "0.3.1" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index ccef0bf4e742..35a86f2e85cd 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -17,8 +17,8 @@ diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64- error-stack = "0.3.1" frunk = "0.4.1" frunk_core = "0.4.1" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 668e8b0574fe..db5b6c37e95d 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -20,9 +20,9 @@ config = { version = "0.13.3", features = ["toml"] } diesel = { version = "2.1.0", features = ["postgres"] } error-stack = "0.3.1" once_cell = "1.18.0" -serde = "1.0.163" -serde_json = "1.0.96" -serde_path_to_error = "0.1.11" +serde = "1.0.193" +serde_json = "1.0.108" +serde_path_to_error = "0.1.14" thiserror = "1.0.40" tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml index 859795964145..08b9f0af28b7 100644 --- a/crates/euclid/Cargo.toml +++ b/crates/euclid/Cargo.toml @@ -12,8 +12,8 @@ frunk_core = "0.4.1" nom = { version = "7.1.3", features = ["alloc"], optional = true } once_cell = "1.18.0" rustc-hash = "1.1.0" -serde = { version = "1.0.163", features = ["derive", "rc"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive", "rc"] } +serde_json = "1.0.108" strum = { version = "0.25", features = ["derive"] } thiserror = "1.0.43" diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 54a636a382b2..4767e4f8d255 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -22,7 +22,7 @@ base64 = "0.21.2" dyn-clone = "1.0.11" error-stack = "0.3.1" once_cell = "1.18.0" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" tokio = "1.28.2" hyper-proxy = "0.9.1" diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index 44a73dae4d77..a07285167e4c 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -16,8 +16,8 @@ euclid = { version = "0.1.0", path = "../euclid" } masking = { version = "0.1.0", path = "../masking/" } # Third party crates -serde = "1.0.163" -serde_json = "1.0.96" +serde = "1.0.193" +serde_json = "1.0.108" thiserror = "1.0.43" [dev-dependencies] diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index bf92e867dc6c..c03de9a1aeed 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -20,9 +20,9 @@ rustdoc-args = ["--cfg", "docsrs"] bytes = { version = "1", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time"], optional = true } serde = { version = "1", features = ["derive"], optional = true } -serde_json = { version = "1.0.96", optional = true } +serde_json = { version = "1.0.108", optional = true } subtle = "=2.4.1" zeroize = { version = "1.6", default-features = false } [dev-dependencies] -serde_json = "1.0.96" +serde_json = "1.0.108" diff --git a/crates/masking/tests/basic.rs b/crates/masking/tests/basic.rs index 7857783f8304..29ba90cbeae5 100644 --- a/crates/masking/tests/basic.rs +++ b/crates/masking/tests/basic.rs @@ -117,7 +117,7 @@ fn without_serialize() -> Result<(), Box> { #[test] fn for_string() -> Result<(), Box> { - #[cfg_attr(feature = "serde", derive(Serialize))] + #[cfg_attr(all(feature = "alloc", feature = "serde"), derive(Serialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Composite { secret_number: Secret, @@ -147,7 +147,7 @@ fn for_string() -> Result<(), Box> { // serialize - #[cfg(feature = "serde")] + #[cfg(all(feature = "alloc", feature = "serde"))] { let got = serde_json::to_string(&composite).unwrap(); let exp = r#"{"secret_number":"abc","not_secret":"not secret"}"#; diff --git a/crates/pm_auth/Cargo.toml b/crates/pm_auth/Cargo.toml index a9aebc5b540a..9654932d5ef0 100644 --- a/crates/pm_auth/Cargo.toml +++ b/crates/pm_auth/Cargo.toml @@ -21,7 +21,7 @@ bytes = "1.4.0" error-stack = "0.3.1" http = "0.2.9" mime = "0.3.17" -serde = "1.0.159" -serde_json = "1.0.91" +serde = "1.0.193" +serde_json = "1.0.108" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.43" diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index 9d3ae724d432..fb8976323f4f 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true error-stack = "0.3.1" fred = { version = "6.3.0", features = ["metrics", "partial-tracing", "subscriber-client"] } futures = "0.3" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" tokio = "1.28.2" diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index e498658e4577..b2f7f8b94a97 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -79,12 +79,12 @@ ring = "0.16.20" roxmltree = "0.18.0" rust_decimal = { version = "1.30.0", features = ["serde-with-float", "serde-with-str"] } rustc-hash = "1.1.0" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" -serde_path_to_error = "0.1.11" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +serde_path_to_error = "0.1.14" serde_qs = { version = "0.12.0", optional = true } serde_urlencoded = "0.7.1" -serde_with = "3.0.0" +serde_with = "3.4.0" sha-1 = { version = "0.9" } sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } strum = { version = "0.25", features = ["derive"] } @@ -134,7 +134,7 @@ rand = "0.8.5" serial_test = "2.0.0" time = { version = "0.3.21", features = ["macros"] } tokio = "1.28.2" -wiremock = "0.5" +wiremock = "0.5.18" # First party dev-dependencies test_utils = { version = "0.1.0", path = "../test_utils" } diff --git a/crates/router_derive/Cargo.toml b/crates/router_derive/Cargo.toml index 6f598e0f0502..07e95b9232c3 100644 --- a/crates/router_derive/Cargo.toml +++ b/crates/router_derive/Cargo.toml @@ -12,7 +12,7 @@ proc-macro = true doctest = false [dependencies] -indexmap = "2.0.0" +indexmap = "2.1.0" proc-macro2 = "1.0.56" quote = "1.0.26" syn = { version = "2.0.5", features = ["full", "extra-traits"] } # the full feature does not seem to encompass all the features @@ -20,6 +20,6 @@ strum = { version = "0.24.1", features = ["derive"] } [dev-dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index 266baf0e3863..cfe8ed561466 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -16,9 +16,9 @@ once_cell = "1.18.0" opentelemetry = { version = "0.19.0", features = ["rt-tokio-current-thread", "metrics"] } opentelemetry-otlp = { version = "0.12.0", features = ["metrics"] } rustc-hash = "1.1" -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" -serde_path_to_error = "0.1.11" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +serde_path_to_error = "0.1.14" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", default-features = false, features = ["formatting"] } tokio = { version = "1.28.2" } diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index 5e8674ab3814..40f7ff7b9474 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -15,8 +15,8 @@ error-stack = "0.3.1" futures = "0.3.28" once_cell = "1.18.0" rand = "0.8.5" -serde = "1.0.163" -serde_json = "1.0.96" +serde = "1.0.193" +serde_json = "1.0.108" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 77589cc7d782..d7f45c91d620 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -41,7 +41,7 @@ mime = "0.3.17" moka = { version = "0.11.3", features = ["future"] } once_cell = "1.18.0" ring = "0.16.20" -serde = { version = "1.0.185", features = ["derive"] } -serde_json = "1.0.105" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" thiserror = "1.0.40" tokio = { version = "1.28.2", features = ["rt-multi-thread"] } diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index 957a51171da7..a95e2e3921bd 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml @@ -18,8 +18,8 @@ base64 = "0.21.2" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } rand = "0.8.5" reqwest = { version = "0.11.18", features = ["native-tls"] } -serde = { version = "1.0.163", features = ["derive"] } -serde_json = "1.0.96" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" serde_urlencoded = "0.7.1" serial_test = "2.0.0" thirtyfour = "0.31.0" From 129b1e55bd1cbad0243030fd25379f1400eb170c Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 11 Dec 2023 15:21:23 +0530 Subject: [PATCH 176/443] chore(deps): update fred and moka (#3088) --- Cargo.lock | 109 ++---------------- config/config.example.toml | 5 + config/development.toml | 17 +++ config/docker_compose.toml | 13 +++ crates/redis_interface/Cargo.toml | 3 +- crates/redis_interface/src/commands.rs | 3 +- crates/redis_interface/src/lib.rs | 71 +++++++++--- crates/redis_interface/src/types.rs | 10 ++ crates/router/src/db/api_keys.rs | 1 + crates/router/src/db/cache.rs | 2 +- .../src/db/merchant_connector_account.rs | 1 + crates/router/tests/cache.rs | 4 +- crates/storage_impl/Cargo.toml | 2 +- crates/storage_impl/src/redis/cache.rs | 18 +-- 14 files changed, 130 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54d37da3a8e6..5f1ab2cd3423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,12 +426,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" -[[package]] -name = "arcstr" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" - [[package]] name = "argon2" version = "0.5.2" @@ -542,26 +536,6 @@ dependencies = [ "tokio 1.32.0", ] -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock", - "autocfg", - "cfg-if 1.0.0", - "concurrent-queue", - "futures-lite", - "log", - "parking", - "polling", - "rustix 0.37.25", - "slab", - "socket2 0.4.9", - "waker-fn", -] - [[package]] name = "async-lock" version = "2.8.0" @@ -2558,16 +2532,14 @@ dependencies = [ [[package]] name = "fred" -version = "6.3.2" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15cc18b56395b8b15ffcdcea7fe8586e3a3ccb3d9dc3b9408800d9814efb08e" +checksum = "f2e8094c30c33132e948eb7e1b740cfdaa5a6702610bd3a2744002ec3575cd68" dependencies = [ "arc-swap", - "arcstr", "async-trait", "bytes 1.5.0", "bytes-utils", - "cfg-if 1.0.0", "float-cmp", "futures 0.3.28", "lazy_static", @@ -2576,7 +2548,7 @@ dependencies = [ "rand 0.8.5", "redis-protocol", "semver 1.0.19", - "sha-1 0.10.1", + "socket2 0.5.4", "tokio 1.32.0", "tokio-stream", "tokio-util", @@ -3278,17 +3250,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "iovec" version = "0.1.4" @@ -3311,7 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.17", + "rustix", "windows-sys", ] @@ -3497,12 +3458,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.8" @@ -3790,12 +3745,12 @@ dependencies = [ [[package]] name = "moka" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6e72583bf6830c956235bff0d5afec8cf2952f579ebad18ae7821a917d950f" +checksum = "d8017ec3548ffe7d4cef7ac0e12b044c01164a74c0f3119420faeaf13490ad8b" dependencies = [ - "async-io", "async-lock", + "async-trait", "crossbeam-channel", "crossbeam-epoch 0.9.15", "crossbeam-utils 0.8.16", @@ -3804,7 +3759,6 @@ dependencies = [ "parking_lot 0.12.1", "quanta", "rustc_version 0.4.0", - "scheduled-thread-pool", "skeptic", "smallvec 1.11.1", "tagptr", @@ -4484,22 +4438,6 @@ dependencies = [ "miniz_oxide 0.3.7", ] -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if 1.0.0", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys", -] - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4874,6 +4812,7 @@ dependencies = [ "serde", "thiserror", "tokio 1.32.0", + "tokio-stream", ] [[package]] @@ -5168,7 +5107,7 @@ dependencies = [ "serde_urlencoded", "serde_with", "serial_test", - "sha-1 0.9.8", + "sha-1", "sqlx", "storage_impl", "strum 0.25.0", @@ -5348,20 +5287,6 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.37.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys", -] - [[package]] name = "rustix" version = "0.38.17" @@ -5371,7 +5296,7 @@ dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.8", + "linux-raw-sys", "windows-sys", ] @@ -5774,17 +5699,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.10.6" @@ -6263,7 +6177,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix 0.38.17", + "rustix", "windows-sys", ] @@ -6641,6 +6555,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio 1.32.0", + "tokio-util", ] [[package]] diff --git a/config/config.example.toml b/config/config.example.toml index eb2574c92ea5..335433077be3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -52,6 +52,11 @@ default_ttl = 300 # Default TTL for entries, in seconds default_hash_ttl = 900 # Default TTL for hashes entries, in seconds use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6) stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options +auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. +disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 0 # An optional timeout to apply to all commands. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. # This section provides configs for currency conversion api [forex_api] diff --git a/config/development.toml b/config/development.toml index dc5423366247..090751b2ea3b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -31,6 +31,23 @@ dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 +[redis] +host = "127.0.0.1" +port = 6379 +pool_size = 5 +reconnect_max_attempts = 5 +reconnect_delay = 5 +default_ttl = 300 +default_hash_ttl = 900 +use_legacy_version = false +stream_read_count = 1 +auto_pipeline = true +disable_auto_backpressure = false +max_in_flight_commands = 5000 +default_command_timeout = 0 +max_feed_count = 200 + + [server] # HTTP Request body limit. Defaults to 32kB request_body_limit = 32768 diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 437df22e300e..dc42a5b44c64 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -73,6 +73,19 @@ host = "redis-standalone" port = 6379 cluster_enabled = false cluster_urls = ["redis-cluster:6379"] +pool_size = 5 +reconnect_max_attempts = 5 +reconnect_delay = 5 +default_ttl = 300 +default_hash_ttl = 900 +use_legacy_version = false +stream_read_count = 1 +auto_pipeline = true +disable_auto_backpressure = false +max_in_flight_commands = 5000 +default_command_timeout = 0 +max_feed_count = 200 + [refund] max_attempts = 10 diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index fb8976323f4f..1a6bc96a7fc4 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -9,11 +9,12 @@ license.workspace = true [dependencies] error-stack = "0.3.1" -fred = { version = "6.3.0", features = ["metrics", "partial-tracing", "subscriber-client"] } +fred = { version = "7.0.0", features = ["metrics", "partial-tracing", "subscriber-client"] } futures = "0.3" serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" tokio = "1.28.2" +tokio-stream = {version = "0.1.14", features = ["sync"]} # First party crates common_utils = { version = "0.1.0", path = "../common_utils", features = ["async_ext"] } diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index ca85d19d38b0..ce2b138d9237 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -383,6 +383,7 @@ impl super::RedisConnectionPool { ) -> CustomResult, errors::RedisError> { Ok(self .pool + .next() .hscan::<&str, &str>(key, pattern, count) .filter_map(|value| async move { match value { @@ -562,7 +563,7 @@ impl super::RedisConnectionPool { .await .into_report() .map_err(|err| match err.current_context().kind() { - RedisErrorKind::NotFound => { + RedisErrorKind::NotFound | RedisErrorKind::Parse => { err.change_context(errors::RedisError::StreamEmptyOrNotAvailable) } _ => err.change_context(errors::RedisError::StreamReadFailed), diff --git a/crates/redis_interface/src/lib.rs b/crates/redis_interface/src/lib.rs index bdc79560118d..7111869a5c03 100644 --- a/crates/redis_interface/src/lib.rs +++ b/crates/redis_interface/src/lib.rs @@ -24,14 +24,14 @@ use std::sync::{atomic, Arc}; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use fred::interfaces::ClientLike; pub use fred::interfaces::PubsubInterface; +use fred::{interfaces::ClientLike, prelude::EventInterface}; use router_env::logger; pub use self::{commands::*, types::*}; pub struct RedisConnectionPool { - pub pool: fred::pool::RedisPool, + pub pool: fred::prelude::RedisPool, config: RedisConfig, pub subscriber: SubscriberClient, pub publisher: RedisClient, @@ -53,8 +53,10 @@ impl RedisClient { pub async fn new( config: fred::types::RedisConfig, reconnect_policy: fred::types::ReconnectPolicy, + perf: fred::types::PerformanceConfig, ) -> CustomResult { - let client = fred::prelude::RedisClient::new(config, None, Some(reconnect_policy)); + let client = + fred::prelude::RedisClient::new(config, Some(perf), None, Some(reconnect_policy)); client.connect(); client .wait_for_connect() @@ -73,8 +75,10 @@ impl SubscriberClient { pub async fn new( config: fred::types::RedisConfig, reconnect_policy: fred::types::ReconnectPolicy, + perf: fred::types::PerformanceConfig, ) -> CustomResult { - let client = fred::clients::SubscriberClient::new(config, None, Some(reconnect_policy)); + let client = + fred::clients::SubscriberClient::new(config, Some(perf), None, Some(reconnect_policy)); client.connect(); client .wait_for_connect() @@ -117,6 +121,17 @@ impl RedisConnectionPool { .into_report() .change_context(errors::RedisError::RedisConnectionError)?; + let perf = fred::types::PerformanceConfig { + auto_pipeline: conf.auto_pipeline, + default_command_timeout: std::time::Duration::from_secs(conf.default_command_timeout), + max_feed_count: conf.max_feed_count, + backpressure: fred::types::BackpressureConfig { + disable_auto_backpressure: conf.disable_auto_backpressure, + max_in_flight_commands: conf.max_in_flight_commands, + policy: fred::types::BackpressurePolicy::Drain, + }, + }; + if !conf.use_legacy_version { config.version = fred::types::RespVersion::RESP3; } @@ -127,13 +142,21 @@ impl RedisConnectionPool { conf.reconnect_delay, ); - let subscriber = SubscriberClient::new(config.clone(), reconnect_policy.clone()).await?; + let subscriber = + SubscriberClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?; - let publisher = RedisClient::new(config.clone(), reconnect_policy.clone()).await?; + let publisher = + RedisClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?; - let pool = fred::pool::RedisPool::new(config, None, Some(reconnect_policy), conf.pool_size) - .into_report() - .change_context(errors::RedisError::RedisConnectionError)?; + let pool = fred::prelude::RedisPool::new( + config, + Some(perf), + None, + Some(reconnect_policy), + conf.pool_size, + ) + .into_report() + .change_context(errors::RedisError::RedisConnectionError)?; pool.connect(); pool.wait_for_connect() @@ -153,16 +176,28 @@ impl RedisConnectionPool { } pub async fn on_error(&self, tx: tokio::sync::oneshot::Sender<()>) { - while let Ok(redis_error) = self.pool.on_error().recv().await { - logger::error!(?redis_error, "Redis protocol or connection error"); - logger::error!("current state: {:#?}", self.pool.state()); - if self.pool.state() == fred::types::ClientState::Disconnected { - if tx.send(()).is_err() { - logger::error!("The redis shutdown signal sender failed to signal"); + use futures::StreamExt; + use tokio_stream::wrappers::BroadcastStream; + + let error_rxs: Vec> = self + .pool + .clients() + .iter() + .map(|client| BroadcastStream::new(client.error_rx())) + .collect(); + + let mut error_rx = futures::stream::select_all(error_rxs); + loop { + if let Some(Ok(error)) = error_rx.next().await { + logger::error!(?error, "Redis protocol or connection error"); + if self.pool.state() == fred::types::ClientState::Disconnected { + if tx.send(()).is_err() { + logger::error!("The redis shutdown signal sender failed to signal"); + } + self.is_redis_available + .store(false, atomic::Ordering::SeqCst); + break; } - self.is_redis_available - .store(false, atomic::Ordering::SeqCst); - break; } } } diff --git a/crates/redis_interface/src/types.rs b/crates/redis_interface/src/types.rs index 5dc93070014e..364fcfabc15a 100644 --- a/crates/redis_interface/src/types.rs +++ b/crates/redis_interface/src/types.rs @@ -52,6 +52,11 @@ pub struct RedisSettings { /// TTL for hash-tables in seconds pub default_hash_ttl: u32, pub stream_read_count: u64, + pub auto_pipeline: bool, + pub disable_auto_backpressure: bool, + pub max_in_flight_commands: u64, + pub default_command_timeout: u64, + pub max_feed_count: u64, } impl RedisSettings { @@ -89,6 +94,11 @@ impl Default for RedisSettings { default_ttl: 300, stream_read_count: 1, default_hash_ttl: 900, + auto_pipeline: true, + disable_auto_backpressure: false, + max_in_flight_commands: 5000, + default_command_timeout: 0, + max_feed_count: 200, } } } diff --git a/crates/router/src/db/api_keys.rs b/crates/router/src/db/api_keys.rs index 4ba9e47e9a5d..94edac969026 100644 --- a/crates/router/src/db/api_keys.rs +++ b/crates/router/src/db/api_keys.rs @@ -535,6 +535,7 @@ mod tests { merchant_id, hashed_api_key.into_inner() ),) + .await .is_none() ) } diff --git a/crates/router/src/db/cache.rs b/crates/router/src/db/cache.rs index 0688665f0c4c..4bda08e22c03 100644 --- a/crates/router/src/db/cache.rs +++ b/crates/router/src/db/cache.rs @@ -63,7 +63,7 @@ where F: FnOnce() -> Fut + Send, Fut: futures::Future> + Send, { - let cache_val = cache.get_val::(key); + let cache_val = cache.get_val::(key).await; if let Some(val) = cache_val { Ok(val) } else { diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 4fbb8f19ccff..3718b962b458 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -890,6 +890,7 @@ mod merchant_connector_account_cache_tests { "{}_{}", merchant_id, connector_label ),) + .await .is_none()) } } diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index 4de45c7132a8..040e0dddf97c 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -50,8 +50,8 @@ async fn invalidate_existing_cache_success() { let response_body = response.body().await; println!("invalidate Cache: {response:?} : {response_body:?}"); assert_eq!(response.status(), awc::http::StatusCode::OK); - assert!(cache::CONFIG_CACHE.get(&cache_key).is_none()); - assert!(cache::ACCOUNTS_CACHE.get(&cache_key).is_none()); + assert!(cache::CONFIG_CACHE.get(&cache_key).await.is_none()); + assert!(cache::ACCOUNTS_CACHE.get(&cache_key).await.is_none()); } #[actix_web::test] diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index d7f45c91d620..0155980d9f7d 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -38,7 +38,7 @@ error-stack = "0.3.1" futures = "0.3.28" http = "0.2.9" mime = "0.3.17" -moka = { version = "0.11.3", features = ["future"] } +moka = { version = "0.12", features = ["future"] } once_cell = "1.18.0" ring = "0.16.20" serde = { version = "1.0.193", features = ["derive"] } diff --git a/crates/storage_impl/src/redis/cache.rs b/crates/storage_impl/src/redis/cache.rs index cd6066627620..a960261f8674 100644 --- a/crates/storage_impl/src/redis/cache.rs +++ b/crates/storage_impl/src/redis/cache.rs @@ -109,7 +109,6 @@ impl Cache { /// `max_capacity`: Max size in MB's that the cache can hold pub fn new(time_to_live: u64, time_to_idle: u64, max_capacity: Option) -> Self { let mut cache_builder = MokaCache::builder() - .eviction_listener_with_queued_delivery_mode(|_, _, _| {}) .time_to_live(std::time::Duration::from_secs(time_to_live)) .time_to_idle(std::time::Duration::from_secs(time_to_idle)); @@ -126,8 +125,8 @@ impl Cache { self.insert(key, Arc::new(val)).await; } - pub fn get_val(&self, key: &str) -> Option { - let val = self.get(key)?; + pub async fn get_val(&self, key: &str) -> Option { + let val = self.get(key).await?; (*val).as_any().downcast_ref::().cloned() } @@ -188,7 +187,7 @@ where F: FnOnce() -> Fut + Send, Fut: futures::Future> + Send, { - let cache_val = cache.get_val::(key); + let cache_val = cache.get_val::(key).await; if let Some(val) = cache_val { Ok(val) } else { @@ -266,14 +265,17 @@ mod cache_tests { async fn construct_and_get_cache() { let cache = Cache::new(1800, 1800, None); cache.push("key".to_string(), "val".to_string()).await; - assert_eq!(cache.get_val::("key"), Some(String::from("val"))); + assert_eq!( + cache.get_val::("key").await, + Some(String::from("val")) + ); } #[tokio::test] async fn eviction_on_size_test() { let cache = Cache::new(2, 2, Some(0)); cache.push("key".to_string(), "val".to_string()).await; - assert_eq!(cache.get_val::("key"), None); + assert_eq!(cache.get_val::("key").await, None); } #[tokio::test] @@ -283,7 +285,7 @@ mod cache_tests { cache.remove("key").await; - assert_eq!(cache.get_val::("key"), None); + assert_eq!(cache.get_val::("key").await, None); } #[tokio::test] @@ -291,6 +293,6 @@ mod cache_tests { let cache = Cache::new(2, 2, None); cache.push("key".to_string(), "val".to_string()).await; tokio::time::sleep(std::time::Duration::from_secs(3)).await; - assert_eq!(cache.get_val::("key"), None); + assert_eq!(cache.get_val::("key").await, None); } } From f7da59d06af11707e210b58a875c013d31c3ee17 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:07:25 +0530 Subject: [PATCH 177/443] fix(router): make `request_incremental_authorization` optional in payment_intent (#3086) --- crates/data_models/src/payments.rs | 2 +- crates/data_models/src/payments/payment_intent.rs | 2 +- crates/diesel_models/src/payment_intent.rs | 4 ++-- crates/diesel_models/src/schema.rs | 2 +- crates/router/src/core/payments/helpers.rs | 9 ++++++--- crates/router/src/core/payments/transformers.rs | 6 ++++-- crates/router/src/core/utils.rs | 12 +++++++----- .../down.sql | 2 ++ .../up.sql | 2 ++ 9 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql create mode 100644 migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index 7a4787fcf0a0..b3e2c2e520a5 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -50,7 +50,7 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, - pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, } diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index d7edcfdf1791..5389cfdd78de 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -107,7 +107,7 @@ pub struct PaymentIntentNew { pub updated_by: String, pub surcharge_applicable: Option, - pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, + pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 1bd5c73a96ca..89d99de2d832 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -52,7 +52,7 @@ pub struct PaymentIntent { pub updated_by: String, pub surcharge_applicable: Option, - pub request_incremental_authorization: RequestIncrementalAuthorization, + pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, } @@ -109,7 +109,7 @@ pub struct PaymentIntentNew { pub payment_confirm_source: Option, pub updated_by: String, pub surcharge_applicable: Option, - pub request_incremental_authorization: RequestIncrementalAuthorization, + pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 9baf613d9233..0d4ab83d8232 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -703,7 +703,7 @@ diesel::table! { #[max_length = 32] updated_by -> Varchar, surcharge_applicable -> Nullable, - request_incremental_authorization -> RequestIncrementalAuthorization, + request_incremental_authorization -> Nullable, incremental_authorization_allowed -> Nullable, authorization_count -> Nullable, } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 866a0581e4e9..e05c60dcf341 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2661,8 +2661,9 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), - request_incremental_authorization: + request_incremental_authorization: Some( common_enums::RequestIncrementalAuthorization::default(), + ), incremental_authorization_allowed: None, authorization_count: None, }; @@ -2715,8 +2716,9 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), - request_incremental_authorization: + request_incremental_authorization: Some( common_enums::RequestIncrementalAuthorization::default(), + ), incremental_authorization_allowed: None, authorization_count: None, }; @@ -2769,8 +2771,9 @@ mod tests { payment_confirm_source: None, surcharge_applicable: None, updated_by: storage_enums::MerchantStorageScheme::PostgresOnly.to_string(), - request_incremental_authorization: + request_incremental_authorization: Some( common_enums::RequestIncrementalAuthorization::default(), + ), incremental_authorization_allowed: None, authorization_count: None, }; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index bd6d03e5625a..5c280ed72d3b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1062,7 +1062,8 @@ impl TryFrom> for types::PaymentsAuthoriz payment_data .payment_intent .request_incremental_authorization, - RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + Some(RequestIncrementalAuthorization::True) + | Some(RequestIncrementalAuthorization::Default) ), }) } @@ -1350,7 +1351,8 @@ impl TryFrom> for types::SetupMandateRequ payment_data .payment_intent .request_incremental_authorization, - RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + Some(RequestIncrementalAuthorization::True) + | Some(RequestIncrementalAuthorization::Default) ), }) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 724a698ff700..50d9be82794b 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1066,8 +1066,8 @@ pub fn get_flow_name() -> RouterResult { pub fn get_request_incremental_authorization_value( request_incremental_authorization: Option, capture_method: Option, -) -> RouterResult { - request_incremental_authorization +) -> RouterResult> { + Some(request_incremental_authorization .map(|request_incremental_authorization| { if request_incremental_authorization { if capture_method == Some(common_enums::CaptureMethod::Automatic) { @@ -1078,14 +1078,16 @@ pub fn get_request_incremental_authorization_value( Ok(RequestIncrementalAuthorization::False) } }) - .unwrap_or(Ok(RequestIncrementalAuthorization::default())) + .unwrap_or(Ok(RequestIncrementalAuthorization::default()))).transpose() } pub fn get_incremental_authorization_allowed_value( incremental_authorization_allowed: Option, - request_incremental_authorization: RequestIncrementalAuthorization, + request_incremental_authorization: Option, ) -> Option { - if request_incremental_authorization == common_enums::RequestIncrementalAuthorization::False { + if request_incremental_authorization + == Some(common_enums::RequestIncrementalAuthorization::False) + { Some(false) } else { incremental_authorization_allowed diff --git a/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql new file mode 100644 index 000000000000..1e7311bedbc5 --- /dev/null +++ b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent ALTER COLUMN request_incremental_authorization SET NOT NULL; \ No newline at end of file diff --git a/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql new file mode 100644 index 000000000000..af2b897aa24b --- /dev/null +++ b/migrations/2023-12-07-075240_make-request-incremental-auth-optional-intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ALTER COLUMN request_incremental_authorization DROP NOT NULL; \ No newline at end of file From fc2f16392148cd66b3c3e67e3e0c782910e37e1f Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:27:14 +0530 Subject: [PATCH 178/443] refactor(email): create client every time of sending email (#3105) --- crates/external_services/src/email/ses.rs | 28 +++++++++++------------ crates/router/src/routes/app.rs | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/external_services/src/email/ses.rs b/crates/external_services/src/email/ses.rs index 7e521a5bc1c4..de4d77949185 100644 --- a/crates/external_services/src/email/ses.rs +++ b/crates/external_services/src/email/ses.rs @@ -12,14 +12,12 @@ use error_stack::{report, IntoReport, ResultExt}; use hyper::Uri; use masking::PeekInterface; use router_env::logger; -use tokio::sync::OnceCell; use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString}; /// Client for AWS SES operation #[derive(Debug, Clone)] pub struct AwsSes { - ses_client: OnceCell, sender: String, settings: EmailSettings, } @@ -70,13 +68,13 @@ pub enum AwsSesError { impl AwsSes { /// Constructs a new AwsSes client pub async fn create(conf: &EmailSettings, proxy_url: Option>) -> Self { + // Build the client initially which will help us know if the email configuration is correct + Self::create_client(conf, proxy_url) + .await + .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) + .ok(); + Self { - ses_client: OnceCell::new_with( - Self::create_client(conf, proxy_url) - .await - .map_err(|error| logger::error!(?error, "Failed to initialize SES Client")) - .ok(), - ), sender: conf.sender_email.clone(), settings: conf.clone(), } @@ -222,13 +220,13 @@ impl EmailClient for AwsSes { body: Self::RichText, proxy_url: Option<&String>, ) -> EmailResult<()> { - self.ses_client - .get_or_try_init(|| async { - Self::create_client(&self.settings, proxy_url) - .await - .change_context(EmailError::ClientBuildingFailure) - }) - .await? + // Not using the same email client which was created at startup as the role session would expire + // Create a client every time when the email is being sent + let email_client = Self::create_client(&self.settings, proxy_url) + .await + .change_context(EmailError::ClientBuildingFailure)?; + + email_client .send_email() .from_email_address(self.sender.to_owned()) .destination( diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index eec8a36ce9ab..fdfd3fc21147 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -897,7 +897,7 @@ impl User { ) .service(web::resource("/forgot_password").route(web::post().to(forgot_password))) .service(web::resource("/reset_password").route(web::post().to(reset_password))) - .service(web::resource("user/invite").route(web::post().to(invite_user))) + .service(web::resource("/user/invite").route(web::post().to(invite_user))) .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), From f87d1f33a8e464a1a3538a2fc681540e5c7e8e01 Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:45:35 +0530 Subject: [PATCH 179/443] ci(Postman): Add refund scenarios for partially captured payment (#3083) --- .../Payment Connector - Create/request.json | 14 ++- .../.meta.json | 12 ++ .../Payments - Capture/.event.meta.json | 5 + .../Payments - Capture/event.test.js | 105 ++++++++++++++++++ .../Payments - Capture/request.json | 45 ++++++++ .../Payments - Capture/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 71 ++++++++++++ .../Payments - Create/request.json | 94 ++++++++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve Copy/.event.meta.json | 5 + .../Payments - Retrieve Copy/event.test.js | 85 ++++++++++++++ .../Payments - Retrieve Copy/request.json | 33 ++++++ .../Payments - Retrieve Copy/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 71 ++++++++++++ .../Payments - Retrieve/request.json | 33 ++++++ .../Payments - Retrieve/response.json | 1 + .../Refunds - Create-copy/.event.meta.json | 5 + .../Refunds - Create-copy/event.test.js | 50 +++++++++ .../Refunds - Create-copy/request.json | 42 +++++++ .../Refunds - Create-copy/response.json | 1 + .../Refunds - Create/.event.meta.json | 5 + .../Refunds - Create/event.test.js | 50 +++++++++ .../Refunds - Create/request.json | 42 +++++++ .../Refunds - Create/response.json | 1 + .../Refunds - Retrieve Copy/.event.meta.json | 5 + .../Refunds - Retrieve Copy/event.test.js | 50 +++++++++ .../Refunds - Retrieve Copy/request.json | 27 +++++ .../Refunds - Retrieve Copy/response.json | 1 + .../.event.meta.json | 5 + .../event.test.js | 41 +++++++ .../request.json | 42 +++++++ .../response.json | 1 + .../Payment Connector - Create/request.json | 20 +++- .../Payment Connector - Create/request.json | 14 ++- .../.meta.json | 12 ++ .../Payments - Capture/.event.meta.json | 5 + .../Payments - Capture/event.test.js | 105 ++++++++++++++++++ .../Payments - Capture/request.json | 45 ++++++++ .../Payments - Capture/response.json | 1 + .../Payments - Create/.event.meta.json | 5 + .../Payments - Create/event.test.js | 71 ++++++++++++ .../Payments - Create/request.json | 92 +++++++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve Copy/.event.meta.json | 5 + .../Payments - Retrieve Copy/event.test.js | 85 ++++++++++++++ .../Payments - Retrieve Copy/request.json | 33 ++++++ .../Payments - Retrieve Copy/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 71 ++++++++++++ .../Payments - Retrieve/request.json | 33 ++++++ .../Payments - Retrieve/response.json | 1 + .../Refunds - Create-copy/.event.meta.json | 5 + .../Refunds - Create-copy/event.test.js | 50 +++++++++ .../Refunds - Create-copy/request.json | 42 +++++++ .../Refunds - Create-copy/response.json | 1 + .../Refunds - Create/.event.meta.json | 5 + .../Refunds - Create/event.test.js | 50 +++++++++ .../Refunds - Create/request.json | 42 +++++++ .../Refunds - Create/response.json | 1 + .../Refunds - Retrieve Copy/.event.meta.json | 5 + .../Refunds - Retrieve Copy/event.test.js | 50 +++++++++ .../Refunds - Retrieve Copy/request.json | 27 +++++ .../Refunds - Retrieve Copy/response.json | 1 + .../.event.meta.json | 5 + .../event.test.js | 41 +++++++ .../request.json | 42 +++++++ .../response.json | 1 + .../Payment Connector - Create/request.json | 14 ++- 70 files changed, 1935 insertions(+), 7 deletions(-) create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json create mode 100644 postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index fe25f6f5e682..375c1f0df52f 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -56,7 +56,12 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -64,7 +69,12 @@ }, { "payment_method_type": "debit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json new file mode 100644 index 000000000000..6626732a3cab --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json @@ -0,0 +1,12 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve", + "Refunds - Create", + "Refunds - Create-copy", + "Refunds - Retrieve Copy", + "Refunds - Validation should throw", + "Payments - Retrieve Copy" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js new file mode 100644 index 000000000000..96d98780785a --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js @@ -0,0 +1,105 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "partially_captured" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} + +// Response body should have value "0" for "amount_received" +if (jsonData?.amount_capturable) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_capturable' matches '0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} + diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json new file mode 100644 index 000000000000..8975575ca40e --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json @@ -0,0 +1,45 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json new file mode 100644 index 000000000000..4cbb79aa56af --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json @@ -0,0 +1,94 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari", + "last_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js new file mode 100644 index 000000000000..3342e5b2530d --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js @@ -0,0 +1,85 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "partially_captured" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Check if the "refunds" array exists +pm.test("Check if 'refunds' array exists", function() { + pm.expect(jsonData.refunds).to.be.an("array"); +}); + +// Check if there are exactly 2 items in the "refunds" array +pm.test("Check if there are 2 refunds", function() { + pm.expect(jsonData.refunds.length).to.equal(2); +}); + + + + diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..89b1355575ae --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "partially_captured" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js new file mode 100644 index 000000000000..b0a888ae70d4 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js @@ -0,0 +1,50 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(2000); + }, + ); +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json new file mode 100644 index 000000000000..ff371b247dbe --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 2000, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js new file mode 100644 index 000000000000..ccc9bf470227 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js @@ -0,0 +1,50 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '4000'", + function () { + pm.expect(jsonData.amount).to.eql(4000); + }, + ); +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json new file mode 100644 index 000000000000..933f1a66edad --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 4000, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js new file mode 100644 index 000000000000..072e259d834a --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js @@ -0,0 +1,50 @@ +// Validate status 2xx +pm.test("[GET]::/refunds/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/refunds/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have value "2000" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '2000'", + function () { + pm.expect(jsonData.amount).to.eql(2000); + }, + ); +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json new file mode 100644 index 000000000000..6c28619e8566 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json @@ -0,0 +1,27 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js new file mode 100644 index 000000000000..71324af2c819 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js @@ -0,0 +1,41 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.error.message) { + pm.test( + "[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'", + function () { + pm.expect(jsonData.error.message).to.eql("Refund amount exceeds the payment amount"); + }, + ); +} + diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json new file mode 100644 index 000000000000..ff371b247dbe --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 2000, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json index ef3bdcd3aa67..4609a0100540 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/bluesnap/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -53,7 +53,25 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 0f14659a5e8f..c0f6521a5c2d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -56,7 +56,12 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -64,7 +69,12 @@ }, { "payment_method_type": "debit", - "card_networks": ["Visa", "Mastercard"], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json new file mode 100644 index 000000000000..6626732a3cab --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json @@ -0,0 +1,12 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve", + "Refunds - Create", + "Refunds - Create-copy", + "Refunds - Retrieve Copy", + "Refunds - Validation should throw", + "Payments - Retrieve Copy" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js new file mode 100644 index 000000000000..96d98780785a --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js @@ -0,0 +1,105 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "partially_captured" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} + +// Response body should have value "0" for "amount_received" +if (jsonData?.amount_capturable) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_capturable' matches '0'", + function () { + pm.expect(jsonData.amount_capturable).to.eql(0); + }, + ); +} + diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json new file mode 100644 index 000000000000..8975575ca40e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json @@ -0,0 +1,45 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json new file mode 100644 index 000000000000..0fbd6a4dcdd5 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json @@ -0,0 +1,92 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js new file mode 100644 index 000000000000..3342e5b2530d --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js @@ -0,0 +1,85 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "partially_captured" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Check if the "refunds" array exists +pm.test("Check if 'refunds' array exists", function() { + pm.expect(jsonData.refunds).to.be.an("array"); +}); + +// Check if there are exactly 2 items in the "refunds" array +pm.test("Check if there are 2 refunds", function() { + pm.expect(jsonData.refunds.length).to.equal(2); +}); + + + + diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..89b1355575ae --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "partially_captured" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js new file mode 100644 index 000000000000..b0a888ae70d4 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js @@ -0,0 +1,50 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(2000); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json new file mode 100644 index 000000000000..ff371b247dbe --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 2000, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js new file mode 100644 index 000000000000..ccc9bf470227 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js @@ -0,0 +1,50 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '4000'", + function () { + pm.expect(jsonData.amount).to.eql(4000); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json new file mode 100644 index 000000000000..933f1a66edad --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 4000, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js new file mode 100644 index 000000000000..072e259d834a --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js @@ -0,0 +1,50 @@ +// Validate status 2xx +pm.test("[GET]::/refunds/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/refunds/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} + +// Response body should have value "2000" for "amount" +if (jsonData?.status) { + pm.test( + "[POST]::/refunds - Content check if value for 'amount' matches '2000'", + function () { + pm.expect(jsonData.amount).to.eql(2000); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json new file mode 100644 index 000000000000..6c28619e8566 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json @@ -0,0 +1,27 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js new file mode 100644 index 000000000000..71324af2c819 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js @@ -0,0 +1,41 @@ +// Validate status 2xx +pm.test("[POST]::/refunds - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.error.message) { + pm.test( + "[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'", + function () { + pm.expect(jsonData.error.message).to.eql("Refund amount exceeds the payment amount"); + }, + ); +} + diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json new file mode 100644 index 000000000000..ff371b247dbe --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json @@ -0,0 +1,42 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 2000, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 474b4d1e1eaf..291a5bf84b51 100644 --- a/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -211,7 +211,12 @@ "maximum_amount": 68607706, "recurring_enabled": true, "installment_payment_enabled": true, - "card_networks": ["Visa", "Mastercard"] + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"] } ] }, @@ -224,7 +229,12 @@ "maximum_amount": 68607706, "recurring_enabled": true, "installment_payment_enabled": true, - "card_networks": ["Visa", "Mastercard"] + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"] } ] }, From ab84f76e0426f3d125acb379f704d7602f96b06c Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:45:26 +0530 Subject: [PATCH 180/443] ci(postman): update postman collection for stripe and trustpay (#3108) --- .../Flow Testcases/Happy Cases/.meta.json | 18 ++-- .../.meta.json | 0 .../Payments - Create}/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 8 +- .../Payments - Create/response.json | 0 .../.event.meta.json | 0 .../Payments - Retrieve-copy/event.test.js | 0 .../Payments - Retrieve-copy}/request.json | 0 .../Payments - Retrieve-copy/response.json | 0 .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve/response.json | 0 .../.event.meta.json | 0 .../Recurring Payments - Create/event.test.js | 0 .../Recurring Payments - Create/request.json | 8 +- .../response.json | 0 .../Refunds - Create-copy/.event.meta.json | 3 - .../Refunds - Create-copy/request.json | 38 --------- .../Refunds - Create/.event.meta.json | 3 - .../Refunds - Retrieve-copy/.event.meta.json | 3 - .../Refunds - Retrieve-copy/request.json | 22 ----- .../Refunds - Retrieve/.event.meta.json | 3 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve-copy/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../.event.meta.json | 3 - .../Payments - Create/request.json | 8 +- .../Payments - Retrieve-copy/.event.meta.json | 4 +- .../Payments - Retrieve-copy/request.json | 9 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../.event.meta.json | 4 +- .../Recurring Payments - Create/request.json | 8 +- .../Refunds - Create Copy/.event.meta.json | 4 +- .../Refunds - Create Copy/request.json | 8 +- .../Refunds - Retrieve Copy/.event.meta.json | 4 +- .../Refunds - Retrieve Copy/request.json | 9 +- .../Scenario19-Bank Debit-ach/.meta.json | 3 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Confirm/.event.meta.json | 5 +- .../Payments - Confirm/event.test.js | 29 ------- .../Payments - Confirm/request.json | 34 +++++++- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 26 ++---- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/event.test.js | 20 ----- .../Payments - Retrieve/request.json | 9 +- .../Scenario20-Bank Debit-ach/.meta.json | 6 ++ .../Payments - Create/.event.meta.json | 6 ++ .../Payments - Create}/event.prerequest.js | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 12 ++- .../Payments - Create}/response.json | 0 .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 9 +- .../Payments - Retrieve}/response.json | 0 .../.meta.json | 0 .../Payments - Confirm/.event.meta.json | 6 ++ .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 10 ++- .../Payments - Confirm}/response.json | 0 .../Payments - Create}/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 8 +- .../Payments - Create/response.json | 0 .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 9 +- .../Payments - Retrieve}/response.json | 0 .../request.json | 9 -- .../request.json | 9 -- .../Payments - Confirm/.event.meta.json | 3 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Retrieve/request.json | 28 ------ .../.meta.json | 3 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Retrieve/request.json | 28 ------ .../.meta.json | 12 --- .../Refunds - Create-copy/event.test.js | 50 ----------- .../Refunds - Create/event.test.js | 50 ----------- .../Refunds - Create/request.json | 42 --------- .../Refunds - Retrieve Copy/event.test.js | 50 ----------- .../event.test.js | 41 --------- .../.meta.json | 0 .../Payments - Confirm/.event.meta.json | 6 ++ .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm}/event.test.js | 43 ++-------- .../Payments - Confirm/request.json | 85 +++++++++++++++++++ .../Payments - Confirm}/response.json | 0 .../Payments - Create/.event.meta.json | 6 ++ .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 30 +++---- .../Payments - Create}/response.json | 0 .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 6 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Retrieve}/response.json | 0 .../.meta.json | 0 .../Payments - Confirm/.event.meta.json | 6 ++ .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/event.test.js | 82 ++++++++++++++++++ .../Payments - Confirm/request.json | 85 +++++++++++++++++++ .../Payments - Confirm}/response.json | 0 .../Payments - Create/.event.meta.json | 6 ++ .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 78 +++++++++++++++++ .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve}/event.test.js | 20 +---- .../Payments - Retrieve}/request.json | 9 +- .../Payments - Retrieve}/response.json | 0 .../Payments - Capture/.event.meta.json | 3 - .../Payments - Capture/request.json | 39 --------- .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Retrieve/request.json | 28 ------ .../.meta.json | 0 .../Payments - Capture/.event.meta.json | 5 ++ .../Payments - Capture/event.test.js | 0 .../Payments - Capture/request.json | 0 .../Payments - Capture}/response.json | 0 .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 33 +++++++ .../Payments - Retrieve/response.json | 0 .../.meta.json | 6 ++ .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 8 +- .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 33 +++++++ .../Payments - Retrieve}/response.json | 0 .../.meta.json | 0 .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 8 +- .../Payments - Create}/response.json | 0 .../Payments - Retrieve-copy/.event.meta.json | 5 ++ .../Payments - Retrieve-copy/event.test.js | 0 .../Payments - Retrieve-copy/request.json | 33 +++++++ .../Payments - Retrieve-copy}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 33 +++++++ .../Payments - Retrieve}/response.json | 0 .../Refunds - Create-copy/.event.meta.json | 5 ++ .../Refunds - Create-copy/event.test.js | 0 .../Refunds - Create-copy}/request.json | 2 +- .../Refunds - Create-copy}/response.json | 0 .../Refunds - Create/.event.meta.json | 5 ++ .../Refunds - Create/event.test.js | 0 .../Refunds - Create}/request.json | 2 +- .../Refunds - Create}/response.json | 0 .../Refunds - Retrieve-copy/.event.meta.json | 5 ++ .../Refunds - Retrieve-copy/event.test.js | 0 .../Refunds - Retrieve-copy}/request.json | 0 .../Refunds - Retrieve-copy}/response.json | 0 .../Refunds - Retrieve/.event.meta.json | 5 ++ .../Refunds - Retrieve/event.test.js | 0 .../Refunds - Retrieve/request.json | 9 +- .../Refunds - Retrieve}/response.json | 0 .../Merchant Account - List/request.json | 14 +-- .../Flow Testcases/Happy Cases/.meta.json | 12 +-- .../Payments - Confirm/.event.meta.json | 3 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Confirm/.event.meta.json | 5 +- .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/request.json | 20 ++++- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 18 ++-- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../.meta.json | 7 ++ .../Payments - Confirm/.event.meta.json | 5 ++ .../Payments - Confirm/event.test.js | 82 ++++++++++++++++++ .../Payments - Confirm/request.json | 85 +++++++++++++++++++ .../Payments - Confirm}/response.json | 0 .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 78 +++++++++++++++++ .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 71 ++++++++++++++++ .../Payments - Retrieve/request.json | 33 +++++++ .../Payments - Retrieve}/response.json | 0 .../.meta.json | 7 ++ .../Payments - Confirm/.event.meta.json | 5 ++ .../Payments - Confirm/event.test.js | 74 ++++++++++++++++ .../Payments - Confirm/request.json | 85 +++++++++++++++++++ .../Payments - Confirm}/response.json | 0 .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 71 ++++++++++++++++ .../Payments - Create/request.json | 78 +++++++++++++++++ .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 71 ++++++++++++++++ .../Payments - Retrieve/request.json | 33 +++++++ .../Payments - Retrieve}/response.json | 0 .../Payments - Confirm/.event.meta.json | 4 +- .../Payments - Confirm/request.json | 10 ++- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Scenario4-Create 3DS payment/.meta.json | 5 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Confirm/.event.meta.json | 5 +- .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/request.json | 12 ++- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Refunds - Create/.event.meta.json | 4 +- .../Refunds - Create/request.json | 8 +- .../Refunds - Retrieve/.event.meta.json | 4 +- .../Refunds - Retrieve/request.json | 9 +- .../Scenario7-Bank Redirect-Ideal/.meta.json | 7 ++ .../Payments - Confirm/.event.meta.json | 5 ++ .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 10 ++- .../Payments - Confirm}/response.json | 0 .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 71 ++++++++++++++++ .../Payments - Create/request.json | 8 +- .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 9 +- .../Payments - Retrieve}/response.json | 0 .../Payments - Confirm/.event.meta.json | 3 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../.meta.json | 7 ++ .../Payments - Confirm/.event.meta.json | 5 ++ .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 10 ++- .../Payments - Confirm}/response.json | 0 .../Payments - Create/.event.meta.json | 5 ++ .../Payments - Create/event.test.js | 71 ++++++++++++++++ .../Payments - Create/request.json | 8 +- .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 5 ++ .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 9 +- .../Payments - Retrieve/response.json | 1 + .../.meta.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create/response.json | 1 + .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve}/request.json | 0 .../Payments - Retrieve/response.json | 1 + .../Refunds - Create}/.event.meta.json | 0 .../Refunds - Create/event.test.js | 0 .../Refunds - Create/request.json | 0 .../Refunds - Create/response.json | 1 + .../Payments - Create/.event.meta.json | 3 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Retrieve/request.json | 28 ------ .../Refunds - Create/.event.meta.json | 3 - .../Refunds - Create/request.json | 38 --------- 291 files changed, 2032 insertions(+), 888 deletions(-) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Capture => Scenario10-Create a mandate and recurring payment/Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/Payments - Create/request.json (97%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario10-Create a mandate and recurring payment}/Payments - Create/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Create => Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/Payments - Retrieve-copy/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy => Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy}/request.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario10-Create a mandate and recurring payment}/Payments - Retrieve-copy/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy => Scenario10-Create a mandate and recurring payment/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund => Scenario10-Create a mandate and recurring payment}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario10-Create a mandate and recurring payment}/Payments - Retrieve/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Retrieve => Scenario10-Create a mandate and recurring payment/Recurring Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/Recurring Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario10-Create a mandate and recurring payment}/Recurring Payments - Create/request.json (96%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund/Refunds - Create-copy => Scenario10-Create a mandate and recurring payment/Recurring Payments - Create}/response.json (100%) delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach => Scenario2-Create payment with confirm false}/Payments - Create/event.prerequest.js (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay/Payments - Confirm => Scenario20-Bank Debit-ach/Payments - Create}/event.prerequest.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach => Scenario20-Bank Debit-ach}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach => Scenario20-Bank Debit-ach}/Payments - Create/request.json (95%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund/Refunds - Create => Scenario20-Bank Debit-ach/Payments - Create}/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Create-copy => Scenario20-Bank Debit-ach/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach => Scenario20-Bank Debit-ach}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario20-Bank Debit-ach}/Payments - Retrieve/request.json (86%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund/Refunds - Retrieve-copy => Scenario20-Bank Debit-ach/Payments - Retrieve}/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay => Scenario21-Wallet-Wechatpay}/.meta.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay => Scenario21-Wallet-Wechatpay}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay => Scenario21-Wallet-Wechatpay}/Payments - Confirm/request.json (92%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund/Refunds - Retrieve => Scenario21-Wallet-Wechatpay/Payments - Confirm}/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Create => Scenario21-Wallet-Wechatpay/Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay => Scenario21-Wallet-Wechatpay}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay => Scenario21-Wallet-Wechatpay}/Payments - Create/request.json (95%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment => Scenario21-Wallet-Wechatpay}/Payments - Create/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy => Scenario21-Wallet-Wechatpay/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay => Scenario21-Wallet-Wechatpay}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario21-Wallet-Wechatpay}/Payments - Retrieve/request.json (86%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy => Scenario21-Wallet-Wechatpay/Payments - Retrieve}/response.json (100%) delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js rename postman/collection-dir/{trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay => stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null}/.meta.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.prerequest.js rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Capture => Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm}/event.test.js (58%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment/Payments - Retrieve => Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.prerequest.js rename postman/collection-dir/{trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay => stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario4-Create payment with manual_multiple capture => Scenario2a-Create payment with confirm false card holder name null}/Payments - Create/request.json (78%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment/Recurring Payments - Create => Scenario2a-Create payment with confirm false card holder name null/Payments - Create}/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Validation should throw => Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund => Scenario2a-Create payment with confirm false card holder name null}/Payments - Retrieve/event.test.js (89%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach => Scenario2a-Create payment with confirm false card holder name null}/Payments - Retrieve/request.json (86%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach/Payments - Create => Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve}/response.json (100%) rename postman/collection-dir/{trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal => stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty}/.meta.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.prerequest.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario19-Bank Debit-ach/Payments - Retrieve => Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.prerequest.js rename postman/collection-dir/{trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal => stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty}/Payments - Create/event.test.js (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay/Payments - Confirm => Scenario2b-Create payment with confirm false card holder name empty/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy => Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve}/event.test.js (77%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy => Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve}/request.json (86%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay/Payments - Create => Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve}/response.json (100%) delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario4-Create payment with manual_multiple capture => Scenario4a-Create payment with manual_multiple capture}/.meta.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario4-Create payment with manual_multiple capture => Scenario4a-Create payment with manual_multiple capture}/Payments - Capture/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund => Scenario4a-Create payment with manual_multiple capture}/Payments - Capture/request.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario22-Wallet-Wechatpay/Payments - Retrieve => Scenario4a-Create payment with manual_multiple capture/Payments - Capture}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund => Scenario4a-Create payment with manual_multiple capture}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund => Scenario4a-Create payment with manual_multiple capture}/Payments - Create/request.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario27-Create a failure card payment with confirm true => Scenario4a-Create payment with manual_multiple capture}/Payments - Create/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario4-Create payment with manual_multiple capture => Scenario4a-Create payment with manual_multiple capture}/Payments - Retrieve/event.test.js (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario27-Create a failure card payment with confirm true => Scenario4a-Create payment with manual_multiple capture}/Payments - Retrieve/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario27-Create a failure card payment with confirm true => Scenario8-Create a failure card payment with confirm true}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario27-Create a failure card payment with confirm true => Scenario8-Create a failure card payment with confirm true}/Payments - Create/request.json (97%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Capture => Scenario8-Create a failure card payment with confirm true/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario27-Create a failure card payment with confirm true => Scenario8-Create a failure card payment with confirm true}/Payments - Retrieve/event.test.js (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Create => Scenario8-Create a failure card payment with confirm true/Payments - Retrieve}/response.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/.meta.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Payments - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Payments - Create/request.json (96%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy => Scenario9a-Partial refund/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Payments - Retrieve-copy/event.test.js (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Payments - Retrieve => Scenario9a-Partial refund/Payments - Retrieve-copy}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Payments - Retrieve/event.test.js (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Create-copy => Scenario9a-Partial refund/Payments - Retrieve}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Refunds - Create-copy/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Validation should throw => Scenario9a-Partial refund/Refunds - Create-copy}/request.json (97%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Create => Scenario9a-Partial refund/Refunds - Create-copy}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Refunds - Create/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Create-copy => Scenario9a-Partial refund/Refunds - Create}/request.json (97%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy => Scenario9a-Partial refund/Refunds - Create}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Refunds - Retrieve-copy/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy => Scenario9a-Partial refund/Refunds - Retrieve-copy}/request.json (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario28-Create partially captured payment with refund/Refunds - Validation should throw => Scenario9a-Partial refund/Refunds - Retrieve-copy}/response.json (100%) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Refunds - Retrieve/event.test.js (100%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario10-Partial refund => Scenario9a-Partial refund}/Refunds - Retrieve/request.json (84%) rename postman/collection-dir/stripe/Flow Testcases/Happy Cases/{Scenario4-Create payment with manual_multiple capture/Payments - Capture => Scenario9a-Partial refund/Refunds - Retrieve}/response.json (100%) delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create => trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture => trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty}/Payments - Create/event.test.js (91%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve => trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay/Payments - Confirm => Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay/Payments - Create => Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/event.test.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay/Payments - Retrieve => Scenario2b-Create payment with confirm false card holder name null/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/request.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal/Payments - Confirm => Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/event.prerequest.js create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal => Scenario7-Bank Redirect-Ideal}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal => Scenario7-Bank Redirect-Ideal}/Payments - Confirm/request.json (93%) rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal/Payments - Create => Scenario7-Bank Redirect-Ideal/Payments - Confirm}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/event.test.js rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay => Scenario7-Bank Redirect-Ideal}/Payments - Create/request.json (97%) rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal/Payments - Retrieve => Scenario7-Bank Redirect-Ideal/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay => Scenario7-Bank Redirect-Ideal}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay => Scenario7-Bank Redirect-Ideal}/Payments - Retrieve/request.json (86%) rename postman/collection-dir/trustpay/Flow Testcases/{Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create => Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve}/response.json (100%) delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay => Scenario8-Bank Redirect-giropay}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-giropay => Scenario8-Bank Redirect-giropay}/Payments - Confirm/request.json (93%) rename postman/collection-dir/trustpay/Flow Testcases/{Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve => Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/event.test.js rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal => Scenario8-Bank Redirect-giropay}/Payments - Create/request.json (97%) rename postman/collection-dir/trustpay/Flow Testcases/{Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create => Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create}/response.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal => Scenario8-Bank Redirect-giropay}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/trustpay/Flow Testcases/Happy Cases/{Scenario8-Bank Redirect-Ideal => Scenario8-Bank Redirect-giropay}/Payments - Retrieve/request.json (86%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json rename postman/collection-dir/trustpay/Flow Testcases/Variation Cases/{Scenario8-Refund for unsuccessful payment => Scenario5-Refund for unsuccessful payment}/.meta.json (100%) rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund => trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/trustpay/Flow Testcases/Variation Cases/{Scenario8-Refund for unsuccessful payment => Scenario5-Refund for unsuccessful payment}/Payments - Create/event.test.js (100%) rename postman/collection-dir/trustpay/Flow Testcases/Variation Cases/{Scenario8-Refund for unsuccessful payment => Scenario5-Refund for unsuccessful payment}/Payments - Create/request.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy => trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/trustpay/Flow Testcases/Variation Cases/{Scenario8-Refund for unsuccessful payment => Scenario5-Refund for unsuccessful payment}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy => trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve}/request.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve => trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create}/.event.meta.json (100%) rename postman/collection-dir/trustpay/Flow Testcases/Variation Cases/{Scenario8-Refund for unsuccessful payment => Scenario5-Refund for unsuccessful payment}/Refunds - Create/event.test.js (100%) rename postman/collection-dir/{stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund => trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment}/Refunds - Create/request.json (100%) create mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json delete mode 100644 postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json index 62945fedcfaa..bfeee020b5d2 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/.meta.json @@ -2,14 +2,18 @@ "childrenOrder": [ "Scenario1-Create payment with confirm true", "Scenario2-Create payment with confirm false", + "Scenario2a-Create payment with confirm false card holder name null", + "Scenario2b-Create payment with confirm false card holder name empty", "Scenario3-Create payment without PMD", "Scenario4-Create payment with Manual capture", + "Scenario4a-Create payment with manual_multiple capture", "Scenario5-Void the payment", "Scenario6-Create 3DS payment", "Scenario7-Create 3DS payment with confrm false", + "Scenario8-Create a failure card payment with confirm true", "Scenario9-Refund full payment", - "Scenario10-Partial refund", - "Scenario11-Create a mandate and recurring payment", + "Scenario9a-Partial refund", + "Scenario10-Create a mandate and recurring payment", "Scenario11-Refund recurring payment", "Scenario12-BNPL-klarna", "Scenario13-BNPL-afterpay", @@ -19,9 +23,13 @@ "Scenario17-Bank Redirect-eps", "Scenario18-Bank Redirect-giropay", "Scenario19-Bank Transfer-ach", - "Scenario19-Bank Debit-ach", - "Scenario22-Wallet-Wechatpay", + "Scenario20-Bank Debit-ach", + "Scenario21-Wallet-Wechatpay", "Scenario22- Update address and List Payment method", - "Scenario23- Update Amount" + "Scenario23- Update Amount", + "Scenario24-Add card flow", + "Scenario25-Don't Pass CVV for save card flow and verifysuccess payment", + "Scenario26-Save card payment with manual capture", + "Scenario27-Create payment without customer_id and with billing address and shipping address" ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/request.json index a5c9391cf748..3ffbe03a6058 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/request.json @@ -98,8 +98,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/request.json similarity index 96% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/request.json index 01f47678beab..613e9148f787 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/request.json @@ -61,8 +61,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Create a mandate and recurring payment/Recurring Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json deleted file mode 100644 index caed78185784..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/request.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "payment_id": "{{payment_id}}", - "amount": 1000, - "reason": "Customer returned product", - "refund_type": "instant", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] - }, - "description": "To create a refund against an already processed payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json deleted file mode 100644 index c4271891fbff..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/request.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json index 599c708ba732..9df18b5e8863 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Create/request.json @@ -86,8 +86,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json index 304d03350584..fe8a73d4581a 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Recurring Payments - Create/request.json @@ -61,8 +61,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Create Copy/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Refund recurring payment/Refunds - Retrieve Copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/.meta.json deleted file mode 100644 index 69b505c6d863..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/.event.meta.json deleted file mode 100644 index 220b1a6723d5..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js", "event.prerequest.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js index dc69bd52a500..f92fba3d0440 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -63,26 +63,6 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "6540" for "amount" -if (jsonData?.amount) { - pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", - function () { - pm.expect(jsonData.amount).to.eql(6540); - }, - ); -} - -// Response body should have value "6540" for "amount_capturable" -if (jsonData?.amount) { - pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", - function () { - pm.expect(jsonData.amount_capturable).to.eql(0); - }, - ); -} - // Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( @@ -92,12 +72,3 @@ if (jsonData?.status) { }, ); } - -// Response body should have "connector_transaction_id" -pm.test( - "[POST]::/payments - Content check if 'connector_transaction_id' exists", - function () { - pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be - .true; - }, -); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json index ec45ef29bb64..540a2fa19461 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -38,13 +38,41 @@ } }, "raw_json_formatted": { - "client_secret": "{{client_secret}}" + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "Joseph Doe", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } } }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.prerequest.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js index 55dc35b91280..0444324000a6 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "requires_confirmation" for "status" +// Response body should have value "requires_payment_method" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'", + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", function () { - pm.expect(jsonData.status).to.eql("requires_confirmation"); + pm.expect(jsonData.status).to.eql("requires_payment_method"); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 4105bd1a869b..b28abd0c3090 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -32,16 +32,6 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", - "payment_method": "card", - "payment_method_data": { - "card": { - "card_number": "4242424242424242", - "card_exp_month": "10", - "card_exp_year": "25", - "card_holder_name": "joseph Doe", - "card_cvc": "123" - } - }, "billing": { "address": { "line1": "1467", @@ -51,7 +41,7 @@ "state": "California", "zip": "94122", "country": "US", - "first_name": "sundari" + "first_name": "PiX" } }, "shipping": { @@ -63,7 +53,7 @@ "state": "California", "zip": "94122", "country": "US", - "first_name": "sundari" + "first_name": "PiX" } }, "statement_descriptor_name": "joseph", @@ -72,17 +62,17 @@ "udf1": "value1", "new_customer": "true", "login_date": "2019-09-10T10:11:12Z" - }, - "routing": { - "type": "single", - "data": "stripe" } } }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js index 53f98b7f7f4d..aced67dbfb78 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js @@ -69,23 +69,3 @@ if (jsonData?.status) { }, ); } - -// Response body should have value "6540" for "amount" -if (jsonData?.amount) { - pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", - function () { - pm.expect(jsonData.amount).to.eql(6540); - }, - ); -} - -// Response body should have value "6540" for "amount_capturable" -if (jsonData?.amount) { - pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'", - function () { - pm.expect(jsonData.amount_capturable).to.eql(0); - }, - ); -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.prerequest.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/request.json similarity index 95% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/request.json index 9612b4909870..eda0801c9cc1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/request.json @@ -23,7 +23,9 @@ "confirm": true, "business_label": "default", "capture_method": "automatic", - "connector": ["stripe"], + "connector": [ + "stripe" + ], "customer_id": "klarna", "capture_on": "2022-09-10T10:11:12Z", "authentication_type": "three_ds", @@ -83,8 +85,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario20-Bank Debit-ach/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/request.json similarity index 92% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/request.json index 9189e4dd8529..42d3653ad96a 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/request.json @@ -50,8 +50,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Confirm/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/request.json similarity index 95% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/request.json index 731eeaf14001..daffd2ab9ec1 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/request.json @@ -49,8 +49,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario21-Wallet-Wechatpay/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json index 060c693c7e19..fed600e09cd9 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant-copy/request.json @@ -32,15 +32,6 @@ "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", "host": [ diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json index 060c693c7e19..fed600e09cd9 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22- Update address and List Payment method/List Payment Methods for a Merchant/request.json @@ -32,15 +32,6 @@ "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", "host": [ diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/.event.meta.json deleted file mode 100644 index 220b1a6723d5..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js", "event.prerequest.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json deleted file mode 100644 index 6cd4b7d96c52..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/request.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/.meta.json deleted file mode 100644 index 69b505c6d863..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json deleted file mode 100644 index 6cd4b7d96c52..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/request.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json deleted file mode 100644 index 6626732a3cab..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/.meta.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "childrenOrder": [ - "Payments - Create", - "Payments - Capture", - "Payments - Retrieve", - "Refunds - Create", - "Refunds - Create-copy", - "Refunds - Retrieve Copy", - "Refunds - Validation should throw", - "Payments - Retrieve Copy" - ] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js deleted file mode 100644 index b0a888ae70d4..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/event.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/refunds - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[POST]::/refunds - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "6540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '6540'", - function () { - pm.expect(jsonData.amount).to.eql(2000); - }, - ); -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js deleted file mode 100644 index ccc9bf470227..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/event.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/refunds - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[POST]::/refunds - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "6540" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '4000'", - function () { - pm.expect(jsonData.amount).to.eql(4000); - }, - ); -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json deleted file mode 100644 index 933f1a66edad..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/request.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "payment_id": "{{payment_id}}", - "amount": 4000, - "reason": "Customer returned product", - "refund_type": "instant", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js deleted file mode 100644 index 072e259d834a..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/event.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// Validate status 2xx -pm.test("[GET]::/refunds/:id - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[GET]::/refunds/:id - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'status' matches 'succeeded'", - function () { - pm.expect(jsonData.status).to.eql("succeeded"); - }, - ); -} - -// Response body should have value "2000" for "amount" -if (jsonData?.status) { - pm.test( - "[POST]::/refunds - Content check if value for 'amount' matches '2000'", - function () { - pm.expect(jsonData.amount).to.eql(2000); - }, - ); -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js deleted file mode 100644 index 71324af2c819..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/refunds - Status code is 4xx", function () { - pm.response.to.be.error; -}); - -// Validate if response header has matching content-type -pm.test("[POST]::/refunds - Content-Type is application/json", function () { - pm.expect(pm.response.headers.get("Content-Type")).to.include( - "application/json", - ); -}); - -// Set response object as internal variable -let jsonData = {}; -try { - jsonData = pm.response.json(); -} catch (e) {} - -// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id -if (jsonData?.refund_id) { - pm.collectionVariables.set("refund_id", jsonData.refund_id); - console.log( - "- use {{refund_id}} as collection variable for value", - jsonData.refund_id, - ); -} else { - console.log( - "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", - ); -} - -// Response body should have value "succeeded" for "status" -if (jsonData?.error.message) { - pm.test( - "[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'", - function () { - pm.expect(jsonData.error.message).to.eql("Refund amount exceeds the payment amount"); - }, - ); -} - diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js similarity index 58% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js index 96d98780785a..f92fba3d0440 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js @@ -1,11 +1,11 @@ // Validate status 2xx -pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { pm.response.to.be.success; }); // Validate if response header has matching content-type pm.test( - "[POST]::/payments/:id/capture - Content-Type is application/json", + "[POST]::/payments/:id/confirm - Content-Type is application/json", function () { pm.expect(pm.response.headers.get("Content-Type")).to.include( "application/json", @@ -14,7 +14,7 @@ pm.test( ); // Validate if response has JSON Body -pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { pm.response.to.have.jsonBody(); }); @@ -63,43 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "partially_captured" for "status" +// Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("partially_captured"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } - -// Response body should have value "6540" for "amount" -if (jsonData?.amount) { - pm.test( - "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", - function () { - pm.expect(jsonData.amount).to.eql(6540); - }, - ); -} - -// Response body should have value "6000" for "amount_received" -if (jsonData?.amount_received) { - pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", - function () { - pm.expect(jsonData.amount_received).to.eql(6000); - }, - ); -} - -// Response body should have value "0" for "amount_received" -if (jsonData?.amount_capturable) { - pm.test( - "[POST]::/payments:id/capture - Content check if value for 'amount_capturable' matches '0'", - function () { - pm.expect(jsonData.amount_capturable).to.eql(0); - }, - ); -} - diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json new file mode 100644 index 000000000000..604ac54144dd --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": null, + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Confirm/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/request.json similarity index 78% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/request.json index 0619498e38c7..b28abd0c3090 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/request.json @@ -20,8 +20,8 @@ "raw_json_formatted": { "amount": 6540, "currency": "USD", - "confirm": true, - "capture_method": "manual", + "confirm": false, + "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", "amount_to_capture": 6540, "customer_id": "StripeCustomer", @@ -32,16 +32,6 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", - "payment_method": "card", - "payment_method_data": { - "card": { - "card_number": "4242424242424242", - "card_exp_month": "10", - "card_exp_year": "25", - "card_holder_name": "joseph Doe", - "card_cvc": "123" - } - }, "billing": { "address": { "line1": "1467", @@ -51,7 +41,7 @@ "state": "California", "zip": "94122", "country": "US", - "first_name": "sundari" + "first_name": "PiX" } }, "shipping": { @@ -63,7 +53,7 @@ "state": "California", "zip": "94122", "country": "US", - "first_name": "sundari" + "first_name": "PiX" } }, "statement_descriptor_name": "joseph", @@ -72,17 +62,17 @@ "udf1": "value1", "new_customer": "true", "login_date": "2019-09-10T10:11:12Z" - }, - "routing": { - "type": "single", - "data": "stripe" } } }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Recurring Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/.event.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js similarity index 89% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js index 89b1355575ae..aced67dbfb78 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "partially_captured" for "status" +// Response body should have value "succeeded" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + "[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("partially_captured"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name null/Payments - Retrieve/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..e8d6b2216c57 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js @@ -0,0 +1,82 @@ +// Validate status 4xx +pm.test("[POST]::/payments - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have "error" +pm.test( + "[POST]::/payments/:id/confirm - Content check if 'error' exists", + function () { + pm.expect(typeof jsonData.error !== "undefined").to.be.true; + }, +); + +// Response body should have value "connector error" for "error type" +if (jsonData?.error?.type) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'", + function () { + pm.expect(jsonData.error.type).to.eql("invalid_request"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json new file mode 100644 index 000000000000..371ef616fe4e --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario19-Bank Debit-ach/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Confirm/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..4ac527d834af --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.prerequest.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Confirm/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js similarity index 77% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js index 3342e5b2530d..44960e9a6a3a 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/event.test.js +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js @@ -60,26 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "partially_captured" for "status" +// Response body should have value "requires_payment_method" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", + "[POST]::/payments:id - Content check if value for 'status' matches 'requires_payment_method'", function () { - pm.expect(jsonData.status).to.eql("partially_captured"); + pm.expect(jsonData.status).to.eql("requires_payment_method"); }, ); } - -// Check if the "refunds" array exists -pm.test("Check if 'refunds' array exists", function() { - pm.expect(jsonData.refunds).to.be.an("array"); -}); - -// Check if there are exactly 2 items in the "refunds" array -pm.test("Check if there are 2 refunds", function() { - pm.expect(jsonData.refunds.length).to.equal(2); -}); - - - - diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario11-Create a mandate and recurring payment/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json deleted file mode 100644 index 9fe257ed85e6..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/request.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "amount_to_capture": 6000, - "statement_descriptor_name": "Joseph", - "statement_descriptor_suffix": "JS" - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json deleted file mode 100644 index 6cd4b7d96c52..000000000000 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/request.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario22-Wallet-Wechatpay/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Capture/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4a-Create payment with manual_multiple capture/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json new file mode 100644 index 000000000000..60051ecca220 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json index 6542d21542da..731b249f2aa6 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json @@ -87,8 +87,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Capture/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/.meta.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/request.json similarity index 96% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/request.json index 2363c62ff27f..b5f464abc14b 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/request.json @@ -81,8 +81,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve Copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Payments - Retrieve/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve-copy/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Payments - Retrieve/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/request.json similarity index 97% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/request.json index ff371b247dbe..b56057fad5dc 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/request.json @@ -19,7 +19,7 @@ }, "raw_json_formatted": { "payment_id": "{{payment_id}}", - "amount": 2000, + "amount": 1000, "reason": "Customer returned product", "refund_type": "instant", "metadata": { diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create-copy/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/request.json similarity index 97% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/request.json index ff371b247dbe..d18aaf8befdf 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Create-copy/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/request.json @@ -19,7 +19,7 @@ }, "raw_json_formatted": { "payment_id": "{{payment_id}}", - "amount": 2000, + "amount": 540, "reason": "Customer returned product", "refund_type": "instant", "metadata": { diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Create/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve-copy/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Retrieve Copy/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/request.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve-copy/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/event.test.js rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/request.json similarity index 84% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/request.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Retrieve/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Capture/response.json rename to postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario9a-Partial refund/Refunds - Retrieve/response.json diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json index 841485a0a048..ed2324e03082 100644 --- a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - List/request.json @@ -27,14 +27,18 @@ } ], "url": { - "raw": "{{baseUrl}}/accounts/list", - "host": ["{{baseUrl}}"], - "path": ["accounts", "list"], + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], "query": [ { "key": "organization_id", - "value": "{{organization_id}}", - "disabled": false + "value": "{{organization_id}}" } ], "variable": [ diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json index 6014e253b0a6..949d82095ec1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/.meta.json @@ -2,11 +2,13 @@ "childrenOrder": [ "Scenario1-Create payment with confirm true", "Scenario2-Create payment with confirm false", - "Scenario3-Create payment without PMD", - "Scenario4-Create 3DS payment", - "Scenario5-Create 3DS payment with confrm false", - "Scenario6-Refund full payment", + "Scenario3-Create payment with confirm false card holder name null", + "Scenario3-Create payment with confirm false card holder name empty", + "Scenario4-Create payment without PMD", + "Scenario5-Create 3DS payment", + "Scenario6-Create 3DS payment with confrm false", + "Scenario7-Refund full payment", "Scenario8-Bank Redirect-Ideal", - "Scenario11-Bank Redirect-giropay" + "Scenario9-Bank Redirect-giropay" ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json index d999cf2d6491..540a2fa19461 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -38,6 +38,16 @@ } }, "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "Joseph Doe", + "card_cvc": "123" + } + }, "client_secret": "{{client_secret}}", "browser_info": { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", @@ -55,8 +65,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js index 55dc35b91280..0444324000a6 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "requires_confirmation" for "status" +// Response body should have value "requires_payment_method" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'", + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", function () { - pm.expect(jsonData.status).to.eql("requires_confirmation"); + pm.expect(jsonData.status).to.eql("requires_payment_method"); }, ); } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index 119353069bd9..b28abd0c3090 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -32,16 +32,6 @@ "description": "Its my first payment request", "authentication_type": "no_three_ds", "return_url": "https://duck.com", - "payment_method": "card", - "payment_method_data": { - "card": { - "card_number": "4242424242424242", - "card_exp_month": "10", - "card_exp_year": "25", - "card_holder_name": "joseph Doe", - "card_cvc": "123" - } - }, "billing": { "address": { "line1": "1467", @@ -77,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..e8d6b2216c57 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/event.test.js @@ -0,0 +1,82 @@ +// Validate status 4xx +pm.test("[POST]::/payments - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have "error" +pm.test( + "[POST]::/payments/:id/confirm - Content check if 'error' exists", + function () { + pm.expect(typeof jsonData.error !== "undefined").to.be.true; + }, +); + +// Response body should have value "connector error" for "error type" +if (jsonData?.error?.type) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'", + function () { + pm.expect(jsonData.error.type).to.eql("invalid_request"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json new file mode 100644 index 000000000000..371ef616fe4e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Confirm/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/event.test.js similarity index 91% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/event.test.js index d683186aa007..0444324000a6 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Create/event.test.js +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "requires_capture" for "status" +// Response body should have value "requires_payment_method" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", function () { - pm.expect(jsonData.status).to.eql("requires_capture"); + pm.expect(jsonData.status).to.eql("requires_payment_method"); }, ); } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario4-Create payment with manual_multiple capture/Payments - Retrieve/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Create/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..44960e9a6a3a --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments:id - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2a-Create payment with confirm false card holder name empty/Payments - Retrieve/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..f92fba3d0440 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/event.test.js @@ -0,0 +1,74 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/confirm - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/confirm - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json new file mode 100644 index 000000000000..604ac54144dd --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/request.json @@ -0,0 +1,85 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": null, + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Confirm/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json new file mode 100644 index 000000000000..b28abd0c3090 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": false, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Create/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..aced67dbfb78 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/request.json new file mode 100644 index 000000000000..b9ebc1be4aa3 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/request.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario2b-Create payment with confirm false card holder name null/Payments - Retrieve/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json index d30e00868a59..8ff62125d1ad 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -65,8 +65,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index e3b1e235042f..b28abd0c3090 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -67,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json index 69b505c6d863..60051ecca220 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json index 7dd4aac02cef..80661beddf2d 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Create/request.json @@ -101,8 +101,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario4-Create 3DS payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json index 0731450e6b25..4ac527d834af 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/event.prerequest.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/event.prerequest.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json index 9fef5309c3a9..fb1c8124ca7e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Confirm/request.json @@ -46,7 +46,7 @@ "card_number": "5200000000000015", "card_exp_month": "03", "card_exp_year": "2030", - "card_holder_name": "", + "card_holder_name": "John Doe", "card_cvc": "737", "card_issuer": "", "card_network": "Visa" @@ -68,8 +68,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json index 3b4c8f74e7f5..0eb2d9f30cf8 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Create/request.json @@ -78,8 +78,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario5-Create 3DS payment with confrm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json index 94473ddb1ec2..c3dddef3a2d5 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Create/request.json @@ -100,8 +100,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario6-Refund full payment/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/request.json similarity index 93% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/request.json index 10b88b517486..c4c248b3bddc 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/request.json @@ -57,8 +57,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Confirm/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/request.json index 0b0c56d26601..87d07a6016b3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/request.json @@ -81,8 +81,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Create/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario7-Bank Redirect-Ideal/Payments - Retrieve/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Confirm/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json new file mode 100644 index 000000000000..57d3f8e2bc7e --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/.meta.json @@ -0,0 +1,7 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm", + "Payments - Retrieve" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/request.json similarity index 93% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/request.json index caf10ddd35db..3f8a95e59bb2 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-giropay/Payments - Confirm/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/request.json @@ -57,8 +57,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Confirm/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/event.test.js new file mode 100644 index 000000000000..0444324000a6 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_payment_method" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", + function () { + pm.expect(jsonData.status).to.eql("requires_payment_method"); + }, + ); +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/request.json similarity index 97% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/request.json index 0b0c56d26601..87d07a6016b3 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Create/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/request.json @@ -81,8 +81,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/response.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Create/response.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..688c85746ef1 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/request.json similarity index 86% rename from postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/request.json index 61fcdaec2d2e..6f4d51c5945e 100644 --- a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-Ideal/Payments - Retrieve/request.json +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Happy Cases/Scenario8-Bank Redirect-giropay/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/.meta.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/.meta.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Create/.event.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/request.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/.event.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve-copy/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/request.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/.event.meta.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/event.test.js similarity index 100% rename from postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/event.test.js rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/event.test.js diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/request.json similarity index 100% rename from postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario10-Partial refund/Refunds - Create/request.json rename to postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/request.json diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario5-Refund for unsuccessful payment/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json deleted file mode 100644 index 6cd4b7d96c52..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Payments - Retrieve/request.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json deleted file mode 100644 index 0731450e6b25..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json b/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json deleted file mode 100644 index 9fe125ce8ea4..000000000000 --- a/postman/collection-dir/trustpay/Flow Testcases/Variation Cases/Scenario8-Refund for unsuccessful payment/Refunds - Create/request.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "payment_id": "{{payment_id}}", - "amount": 540, - "reason": "Customer returned product", - "refund_type": "instant", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] - }, - "description": "To create a refund against an already processed payment" -} From a7b688aac72e15f782046b9d108aca12f43a9994 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:03:21 +0530 Subject: [PATCH 181/443] feat(connector): [Placetopay] Add Connector Template Code (#3084) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + connector-template/transformers.rs | 2 - crates/api_models/src/enums.rs | 1 + crates/common_enums/src/enums.rs | 1 + crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 9 +- crates/router/src/connector/placetopay.rs | 545 ++++++++++++++++++ .../src/connector/placetopay/transformers.rs | 236 ++++++++ crates/router/src/core/admin.rs | 4 + crates/router/src/core/payments/flows.rs | 26 + crates/router/src/types/api.rs | 1 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/main.rs | 1 + crates/router/tests/connectors/placetopay.rs | 420 ++++++++++++++ .../router/tests/connectors/sample_auth.toml | 4 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + openapi/openapi_spec.json | 1 + scripts/add_connector.sh | 2 +- 21 files changed, 1256 insertions(+), 7 deletions(-) create mode 100644 crates/router/src/connector/placetopay.rs create mode 100644 crates/router/src/connector/placetopay/transformers.rs create mode 100644 crates/router/tests/connectors/placetopay.rs diff --git a/config/config.example.toml b/config/config.example.toml index 335433077be3..1b720eaeb42c 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -216,6 +216,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/config/development.toml b/config/development.toml index 090751b2ea3b..1ce26053fc77 100644 --- a/config/development.toml +++ b/config/development.toml @@ -130,6 +130,7 @@ cards = [ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "shift4", @@ -202,6 +203,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index dc42a5b44c64..00840daf55fe 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -138,6 +138,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" @@ -201,6 +202,7 @@ cards = [ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "shift4", diff --git a/connector-template/transformers.rs b/connector-template/transformers.rs index bdbfb2e45672..60b13693054d 100644 --- a/connector-template/transformers.rs +++ b/connector-template/transformers.rs @@ -42,7 +42,6 @@ pub struct {{project-name | downcase | pascal_case}}PaymentsRequest { #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct {{project-name | downcase | pascal_case}}Card { - name: Secret, number: cards::CardNumber, expiry_month: Secret, expiry_year: Secret, @@ -56,7 +55,6 @@ impl TryFrom<&{{project-name | downcase | pascal_case}}RouterData<&types::Paymen match item.router_data.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => { let card = {{project-name | downcase | pascal_case}}Card { - name: req_card.card_holder_name, number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 215860540555..f1b4447c3316 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -109,6 +109,7 @@ pub enum Connector { Payme, Paypal, Payu, + Placetopay, Powertranz, Prophetpay, Rapyd, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 980f98db1519..a749744e06df 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -142,6 +142,7 @@ pub enum RoutableConnectors { Payme, Paypal, Payu, + Placetopay, Powertranz, Prophetpay, Rapyd, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 1c885e90cc75..5b34d56c9388 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -614,6 +614,7 @@ pub struct Connectors { pub payme: ConnectorParams, pub paypal: ConnectorParams, pub payu: ConnectorParams, + pub placetopay: ConnectorParams, pub powertranz: ConnectorParams, pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 55c61442591d..e336d8e3514f 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -36,6 +36,7 @@ pub mod payeezy; pub mod payme; pub mod paypal; pub mod payu; +pub mod placetopay; pub mod powertranz; pub mod prophetpay; pub mod rapyd; @@ -63,8 +64,8 @@ pub use self::{ globalpay::Globalpay, globepay::Globepay, gocardless::Gocardless, helcim::Helcim, iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, - payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, powertranz::Powertranz, - prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, signifyd::Signifyd, square::Square, - stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, - worldline::Worldline, worldpay::Worldpay, zen::Zen, + payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, placetopay::Placetopay, + powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, + signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, + volt::Volt, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/placetopay.rs b/crates/router/src/connector/placetopay.rs new file mode 100644 index 000000000000..71f07d79fb6d --- /dev/null +++ b/crates/router/src/connector/placetopay.rs @@ -0,0 +1,545 @@ +pub mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use transformers as placetopay; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorIntegration, ConnectorValidation, + }, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Placetopay; + +impl api::Payment for Placetopay {} +impl api::PaymentSession for Placetopay {} +impl api::ConnectorAccessToken for Placetopay {} +impl api::MandateSetup for Placetopay {} +impl api::PaymentAuthorize for Placetopay {} +impl api::PaymentSync for Placetopay {} +impl api::PaymentCapture for Placetopay {} +impl api::PaymentVoid for Placetopay {} +impl api::Refund for Placetopay {} +impl api::RefundExecute for Placetopay {} +impl api::RefundSync for Placetopay {} +impl api::PaymentToken for Placetopay {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Placetopay +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Placetopay +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Placetopay { + fn id(&self) -> &'static str { + "placetopay" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.placetopay.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = placetopay::PlacetopayAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayErrorResponse = res + .response + .parse_struct("PlacetopayErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Placetopay {} + +impl ConnectorIntegration + for Placetopay +{ +} + +impl ConnectorIntegration + for Placetopay +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Placetopay +{ +} + +impl ConnectorIntegration + for Placetopay +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = placetopay::PlacetopayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let req_obj = placetopay::PlacetopayPaymentsRequest::try_from(&connector_router_data)?; + let placetopay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(placetopay_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, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("Placetopay PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("placetopay PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + 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 get_url( + &self, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("Placetopay PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ +} + +impl ConnectorIntegration + for Placetopay +{ + 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_url( + &self, + _req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = placetopay::PlacetopayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.refund_amount, + req, + ))?; + let req_obj = placetopay::PlacetopayRefundRequest::try_from(&connector_router_data)?; + let placetopay_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(placetopay_req)) + } + + 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)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: placetopay::RefundResponse = res + .response + .parse_struct("placetopay RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Placetopay +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::RefundResponse = res + .response + .parse_struct("placetopay RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Placetopay { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/placetopay/transformers.rs b/crates/router/src/connector/placetopay/transformers.rs new file mode 100644 index 000000000000..e947c6830c35 --- /dev/null +++ b/crates/router/src/connector/placetopay/transformers.rs @@ -0,0 +1,236 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::PaymentsAuthorizeRequestData, + core::errors, + types::{self, api, storage::enums}, +}; + +pub struct PlacetopayRouterData { + pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PlacetopayRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount, + router_data: item, + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct PlacetopayPaymentsRequest { + amount: i64, + card: PlacetopayCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct PlacetopayCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>> + for PlacetopayPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + api::PaymentMethodData::Card(req_card) => { + let card = PlacetopayCard { + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.router_data.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.amount.to_owned(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +// Auth Struct +pub struct PlacetopayAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for PlacetopayAuthType { + 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_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} +// PaymentsResponse +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PlacetopayPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: PlacetopayPaymentStatus) -> Self { + match item { + PlacetopayPaymentStatus::Succeeded => Self::Charged, + PlacetopayPaymentStatus::Failed => Self::Failure, + PlacetopayPaymentStatus::Processing => Self::Authorizing, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PlacetopayPaymentsResponse { + status: PlacetopayPaymentStatus, + id: String, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PlacetopayPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } +} +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct PlacetopayRefundRequest { + pub amount: i64, +} + +impl TryFrom<&PlacetopayRouterData<&types::RefundsRouterData>> for PlacetopayRefundRequest { + type Error = error_stack::Report; + fn try_from( + item: &PlacetopayRouterData<&types::RefundsRouterData>, + ) -> Result { + Ok(Self { + amount: item.amount.to_owned(), + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct PlacetopayErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 113bc7d677d2..f5bb357af0ba 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1795,6 +1795,10 @@ pub(crate) fn validate_auth_and_metadata_type( payu::transformers::PayuAuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Placetopay => { + placetopay::transformers::PlacetopayAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Powertranz => { powertranz::transformers::PowertranzAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 81ba48e9831f..0cb91de05992 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -171,6 +171,7 @@ default_imp_for_complete_authorize!( connector::Opennode, connector::Payeezy, connector::Payu, + connector::Placetopay, connector::Rapyd, connector::Signifyd, connector::Square, @@ -246,6 +247,7 @@ default_imp_for_webhook_source_verification!( connector::Payeezy, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -326,6 +328,7 @@ default_imp_for_create_customer!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -395,6 +398,7 @@ default_imp_for_connector_redirect_response!( connector::Opennode, connector::Payeezy, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -455,6 +459,7 @@ default_imp_for_connector_request_id!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -538,6 +543,7 @@ default_imp_for_accept_dispute!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -639,6 +645,7 @@ default_imp_for_file_upload!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -718,6 +725,7 @@ default_imp_for_submit_evidence!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -797,6 +805,7 @@ default_imp_for_defend_dispute!( connector::Paypal, connector::Payme, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -875,6 +884,7 @@ default_imp_for_pre_processing_steps!( connector::Opennode, connector::Payeezy, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -937,6 +947,7 @@ default_imp_for_payouts!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1017,6 +1028,7 @@ default_imp_for_payouts_create!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1100,6 +1112,7 @@ default_imp_for_payouts_eligibility!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1180,6 +1193,7 @@ default_imp_for_payouts_fulfill!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1260,6 +1274,7 @@ default_imp_for_payouts_cancel!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1341,6 +1356,7 @@ default_imp_for_payouts_quote!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1422,6 +1438,7 @@ default_imp_for_payouts_recipient!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1502,6 +1519,7 @@ default_imp_for_approve!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1583,6 +1601,7 @@ default_imp_for_reject!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1648,6 +1667,7 @@ default_imp_for_fraud_check!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1730,6 +1750,7 @@ default_imp_for_frm_sale!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1812,6 +1833,7 @@ default_imp_for_frm_checkout!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1894,6 +1916,7 @@ default_imp_for_frm_transaction!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -1976,6 +1999,7 @@ default_imp_for_frm_fulfillment!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -2058,6 +2082,7 @@ default_imp_for_frm_record_return!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, @@ -2137,6 +2162,7 @@ default_imp_for_incremental_authorization!( connector::Payme, connector::Paypal, connector::Payu, + connector::Placetopay, connector::Powertranz, connector::Prophetpay, connector::Rapyd, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 978ce078faf9..71751adf6ba4 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -388,6 +388,7 @@ impl ConnectorData { // "payeezy" => Ok(Box::new(&connector::Payeezy)), As psync and rsync are not supported by this connector, it is added as template code for future usage enums::Connector::Payme => Ok(Box::new(&connector::Payme)), enums::Connector::Payu => Ok(Box::new(&connector::Payu)), + enums::Connector::Placetopay => Ok(Box::new(&connector::Placetopay)), enums::Connector::Powertranz => Ok(Box::new(&connector::Powertranz)), enums::Connector::Prophetpay => Ok(Box::new(&connector::Prophetpay)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 34ae3dceb5ab..9f5d46aba426 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -212,6 +212,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Payme => Self::Payme, api_enums::Connector::Paypal => Self::Paypal, api_enums::Connector::Payu => Self::Payu, + api_models::enums::Connector::Placetopay => Self::Placetopay, api_enums::Connector::Plaid => { Err(common_utils::errors::ValidationError::InvalidValue { message: "plaid is not a routable connector".to_string(), diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index fc474818b505..8db743d6098e 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -45,6 +45,7 @@ mod payeezy; mod payme; mod paypal; mod payu; +mod placetopay; mod powertranz; #[cfg(feature = "dummy_connector")] mod prophetpay; diff --git a/crates/router/tests/connectors/placetopay.rs b/crates/router/tests/connectors/placetopay.rs new file mode 100644 index 000000000000..b7c70c789da2 --- /dev/null +++ b/crates/router/tests/connectors/placetopay.rs @@ -0,0 +1,420 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct PlacetopayTest; +impl ConnectorActions for PlacetopayTest {} +impl utils::Connector for PlacetopayTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Placetopay; + types::api::ConnectorData { + connector: Box::new(&Placetopay), + connector_name: types::Connector::Placetopay, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .placetopay + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "placetopay".to_string() + } +} + +static CONNECTOR: PlacetopayTest = PlacetopayTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index f8f6039d6d36..ff179f745065 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -189,3 +189,7 @@ api_key="API Key" api_key = "MyApiKey" key1 = "Merchant id" api_secret = "Secret key" + +[placetopay] +api_key= "Login" +key1= "Trankey" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 9562972c126e..d95d6ed94f1f 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -48,6 +48,7 @@ pub struct ConnectorAuthentication { pub payme: Option, pub paypal: Option, pub payu: Option, + pub placetopay: Option, pub powertranz: Option, pub prophetpay: Option, pub rapyd: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index f4070beb943d..c8c6b1921183 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -111,6 +111,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" @@ -173,6 +174,7 @@ cards = [ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "shift4", diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index f77638a43db5..3c0206a15896 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4380,6 +4380,7 @@ "payme", "paypal", "payu", + "placetopay", "powertranz", "prophetpay", "rapyd", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 1246c51d8eb3..c1f59cb36359 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp From aa9782164fb7846fe533c5057a17756dc82ede54 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:52:16 +0000 Subject: [PATCH 182/443] test(postman): update postman collection files --- .../adyen_uk.postman_collection.json | 2 +- .../bluesnap.postman_collection.json | 912 +- .../checkout.postman_collection.json | 2 +- .../stripe.postman_collection.json | 9899 +++++++++-------- .../trustpay.postman_collection.json | 1359 ++- 5 files changed, 7336 insertions(+), 4838 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 04a7e39f15e7..330661b231d5 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -472,7 +472,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index 34ad07ae67a3..0da43b54cdcd 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -449,7 +449,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"bluesnap\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"test_mode\":false,\"disabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"bluesnap\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"test_mode\":false,\"disabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -4199,6 +4199,916 @@ } ] }, + { + "name": "Scenario28-Create partially captured payment with refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"partially_captured\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"0\" for \"amount_received\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_capturable' matches '0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"partially_captured\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '4000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(4000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":4000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(2000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"2000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '2000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(2000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Validation should throw", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.error.message) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Refund amount exceeds the payment amount\");", + " },", + " );", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"partially_captured\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Check if the \"refunds\" array exists", + "pm.test(\"Check if 'refunds' array exists\", function() {", + " pm.expect(jsonData.refunds).to.be.an(\"array\");", + "});", + "", + "// Check if there are exactly 2 items in the \"refunds\" array", + "pm.test(\"Check if there are 2 refunds\", function() {", + " pm.expect(jsonData.refunds.length).to.equal(2);", + "});", + "", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, { "name": "Scenario8-Refund full payment", "item": [ diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index 54892f116a0e..d901fc39b91e 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -424,7 +424,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 4d3e548f535f..82ffad6f2f59 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -423,7 +423,7 @@ } ], "url": { - "raw": "{{baseUrl}}/accounts/list", + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", "host": [ "{{baseUrl}}" ], @@ -434,8 +434,7 @@ "query": [ { "key": "organization_id", - "value": "{{organization_id}}", - "disabled": false + "value": "{{organization_id}}" } ], "variable": [ @@ -5665,7 +5664,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"Visa\",\"Mastercard\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"Visa\",\"Mastercard\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -6055,7 +6054,7 @@ "name": "Happy Cases", "item": [ { - "name": "Scenario24-Add card flow", + "name": "Scenario1-Create payment with confirm true", "item": [ { "name": "Payments - Create", @@ -6064,62 +6063,87 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", - " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " })};" + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6144,7 +6168,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4111111111111111\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6160,32 +6184,93 @@ "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" ], "type": "text/javascript" } @@ -6200,126 +6285,125 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ { - "name": "Save card payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" ], "type": "text/javascript" } @@ -6344,7 +6428,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -6360,7 +6444,7 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Confirm", "event": [ { "listen": "test", @@ -6440,17 +6524,15 @@ " },", " );", "}", - "", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -6496,7 +6578,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -6539,17 +6621,17 @@ " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", @@ -6588,6 +6670,16 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -6603,7 +6695,7 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], @@ -6611,6 +6703,12 @@ "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", @@ -6622,48 +6720,103 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario2a-Create payment with confirm false card holder name null", + "item": [ { - "name": "Refunds - Create Copy", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", " );", "}", "" ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { @@ -6685,38 +6838,46 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -6725,115 +6886,1294 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2b-Create payment with confirm false card holder name empty", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario25-Don't Pass CVV for save card flow and verifysuccess payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6858,48 +8198,109 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To capture the funds for an uncaptured payment" }, "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -6914,126 +8315,116 @@ } ], "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" + "payments", + ":id" ], "query": [ { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true + "key": "force_sync", + "value": "true" } ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario4a-Create payment with manual_multiple capture", + "item": [ { - "name": "Save card payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -7058,7 +8449,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -7074,20 +8465,20 @@ "response": [] }, { - "name": "Save card payments - Confirm", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", " function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", @@ -7096,7 +8487,7 @@ ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7145,47 +8536,42 @@ " );", "}", "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " })};" + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -7204,17 +8590,17 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "confirm" + "capture" ], "variable": [ { @@ -7224,142 +8610,155 @@ } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To capture the funds for an uncaptured payment" }, "response": [] - } - ] - }, - { - "name": "Scenario26-Save card payment with manual capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", - "}" + "}", + "" ], "type": "text/javascript" } } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7408,47 +8807,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_capture\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", " },", " );", "}", @@ -7459,264 +8823,301 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "List payment methods for a Customer", + "name": "Payments - Cancel", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + }, "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "raw": "{{baseUrl}}/payments/:id/cancel", "host": [ "{{baseUrl}}" ], "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } + "payments", + ":id", + "cancel" ], "variable": [ { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" }, "response": [] }, { - "name": "Save card payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario6-Create 3DS payment", + "item": [ { - "name": "Save card payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7765,26 +9166,24 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -7792,26 +9191,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -7830,54 +9209,42 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -7926,47 +9293,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -7977,35 +9309,27 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "capture" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -8015,31 +9339,36 @@ } ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario7-Create 3DS payment with confrm false", + "item": [ { - "name": "Payments - Retrieve-copy", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -8088,47 +9417,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_confirmation\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", " },", " );", "}", @@ -8139,57 +9433,64 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Create Copy", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -8198,43 +9499,72 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -8242,6 +9572,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -8260,75 +9610,105 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Retrieve Copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -8347,30 +9727,36 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario27-Create a failure card payment with confirm true", + "name": "Scenario8-Create a failure card payment with confirm true", "item": [ { "name": "Payments - Create", @@ -8670,7 +10056,7 @@ ] }, { - "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "name": "Scenario9-Refund full payment", "item": [ { "name": "Payments - Create", @@ -8750,15 +10136,6 @@ " },", " );", "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", "" ], "type": "text/javascript" @@ -8784,7 +10161,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -8877,15 +10254,6 @@ " },", " );", "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", "" ], "type": "text/javascript" @@ -8926,231 +10294,64 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with manual_multiple capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", + "if (jsonData?.status) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -9176,105 +10377,75 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", @@ -9293,36 +10464,30 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario1-Create payment with confirm true", + "name": "Scenario9a-Partial refund", "item": [ { "name": "Payments - Create", @@ -9402,15 +10567,6 @@ " },", " );", "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", "" ], "type": "text/javascript" @@ -9436,7 +10592,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -9529,15 +10685,6 @@ " },", " );", "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", "" ], "type": "text/javascript" @@ -9578,87 +10725,158 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.amount).to.eql(540);", " },", " );", "}", @@ -9669,64 +10887,51 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Confirm", + "name": "Refunds - Create-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", @@ -9735,92 +10940,38 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"1000\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.amount).to.eql(1000);", " },", " );", "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ "" ], "type": "text/javascript" @@ -9828,26 +10979,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -9866,125 +10997,75 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "refunds" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", + " pm.expect(jsonData.amount).to.eql(1000);", " },", " );", "}", @@ -10003,58 +11084,47 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10103,15 +11173,20 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", "" ], "type": "text/javascript" @@ -10119,63 +11194,66 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario10-Create a mandate and recurring payment", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10223,24 +11301,42 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", + "", "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -10248,26 +11344,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -10286,27 +11362,18 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, @@ -10379,15 +11446,31 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -10428,14 +11511,9 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture", - "item": [ + }, { - "name": "Payments - Create", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", @@ -10503,15 +11581,39 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -10537,7 +11639,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -10553,29 +11655,26 @@ "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10624,35 +11723,31 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -10660,35 +11755,27 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id", - "capture" + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } ], "variable": [ { @@ -10698,31 +11785,36 @@ } ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario11-Refund recurring payment", + "item": [ { - "name": "Payments - Retrieve", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10780,6 +11872,32 @@ " },", " );", "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -10787,66 +11905,60 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario5-Void the payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10895,15 +12007,31 @@ " );", "}", "", - "// Response body should have value \"requires_capture\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -10911,63 +12039,61 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] }, { - "name": "Payments - Cancel", + "name": "Recurring Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -10977,16 +12103,29 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", @@ -11003,15 +12142,49 @@ " );", "}", "", - "// Response body should have value \"cancelled\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -11037,32 +12210,23 @@ "language": "json" } }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", @@ -11130,15 +12294,31 @@ " );", "}", "", - "// Response body should have value \"cancelled\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", "" ], "type": "text/javascript" @@ -11179,99 +12359,64 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", "" ], "type": "text/javascript" @@ -11297,96 +12442,75 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"6540\" for \"amount\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.amount).to.eql(6540);", " },", " );", "}", @@ -11405,36 +12529,30 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario7-Create 3DS payment with confrm false", + "name": "Scenario12-BNPL-klarna", "item": [ { "name": "Payments - Create", @@ -11505,12 +12623,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -11539,7 +12657,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -11638,12 +12756,32 @@ "", "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", " function () {", " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", " .true;", " },", ");", + "", + "// Response body should have value \"klarna\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'klarna'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"klarna\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -11698,7 +12836,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"klarna_redirect\":{\"issuer_name\":\"stripe\",\"billing_email\":\"arjun.karthik@juspay.in\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -11844,7 +12982,7 @@ ] }, { - "name": "Scenario9-Refund full payment", + "name": "Scenario13-BNPL-afterpay", "item": [ { "name": "Payments - Create", @@ -11915,12 +13053,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -11949,7 +13087,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"SE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"SE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -11965,26 +13103,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -12033,110 +13174,41 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"afterpay_clearpay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'afterpay_clearpay'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"afterpay_clearpay\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", @@ -12144,9 +13216,38 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -12165,75 +13266,105 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"afterpay_clearpay_redirect\":{\"billing_name\":\"Akshaya\",\"billing_email\":\"example@example.com\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -12252,30 +13383,36 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario10-Partial refund", + "name": "Scenario14-BNPL-affirm", "item": [ { "name": "Payments - Create", @@ -12346,12 +13483,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -12380,7 +13517,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -12396,26 +13533,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -12464,110 +13604,41 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"affirm\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'affirm'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"affirm\");", " },", " );", "}", "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", @@ -12575,9 +13646,38 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -12596,75 +13696,105 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"affirm_redirect\":{\"issuer_name\":\"affirm\",\"billing_email\":\"user-us@example.com\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Retrieve", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -12683,80 +13813,112 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario15-Bank Redirect-Ideal", + "item": [ { - "name": "Refunds - Create-copy", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"1000\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -12785,38 +13947,46 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments" ] }, - "description": "To create a refund against an already processed payment" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Refunds - Retrieve-copy", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", @@ -12825,35 +13995,80 @@ " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"ideal\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", + " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", @@ -12861,39 +14076,82 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { - "method": "GET", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + }, "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", - ":id" + "payments", + ":id", + "confirm" ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -12961,20 +14219,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", "" ], "type": "text/javascript" @@ -13019,7 +14272,7 @@ ] }, { - "name": "Scenario11-Create a mandate and recurring payment", + "name": "Scenario16-Bank Redirect-sofort", "item": [ { "name": "Payments - Create", @@ -13090,41 +14343,15 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -13150,7 +14377,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -13166,26 +14393,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -13234,174 +14464,53 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + "// Response body should have value \"sofort\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", + " },", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -13409,6 +14518,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -13427,23 +14556,32 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_noe_lb_fur_niederosterreich_u_wien\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", @@ -13511,31 +14649,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -13580,7 +14702,7 @@ ] }, { - "name": "Scenario11-Refund recurring payment", + "name": "Scenario17-Bank Redirect-eps", "item": [ { "name": "Payments - Create", @@ -13651,41 +14773,174 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", + "// Response body should have value \"eps\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript" @@ -13693,6 +14948,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -13711,18 +14986,27 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_oberosterreich_salzburg_steiermark\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, @@ -13795,31 +15079,15 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -13860,9 +15128,14 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario18-Bank Redirect-giropay", + "item": [ { - "name": "Recurring Payments - Create", + "name": "Payments - Create", "event": [ { "listen": "test", @@ -13930,49 +15203,15 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", "" ], "type": "text/javascript" @@ -13998,7 +15237,7 @@ "language": "json" } }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -14014,26 +15253,29 @@ "response": [] }, { - "name": "Payments - Retrieve-copy", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -14082,126 +15324,41 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", " },", ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", " },", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", @@ -14209,9 +15366,38 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -14230,75 +15416,105 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Refunds - Retrieve Copy", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", " );", "}", "", - "// Response body should have value \"6540\" for \"amount\"", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -14317,30 +15533,36 @@ } ], "url": { - "raw": "{{baseUrl}}/refunds/:id", + "raw": "{{baseUrl}}/payments/:id?force_sync=true", "host": [ "{{baseUrl}}" ], "path": [ - "refunds", + "payments", ":id" ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario12-BNPL-klarna", + "name": "Scenario19-Bank Transfer-ach", "item": [ { "name": "Payments - Create", @@ -14445,7 +15667,7 @@ "language": "json" } }, - "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://hs-payments-test.netlify.app/payments\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -14542,21 +15764,32 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"next_action.type\"", "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " \"[POST]::/payments - Content check if 'next_action.type' exists\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", " },", ");", "", - "// Response body should have value \"klarna\" for \"payment_method_type\"", + "// Response body should have value \"ach\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'klarna'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"klarna\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " },", + " );", + "}", + "", + "// Response body should have value \"display_bank_transfer_information\" for \"next_action.type\"", + "if (jsonData?.next_action.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'display_bank_transfer_information'\",", + " function () {", + " pm.expect(jsonData.next_action.type).to.eql(", + " \"display_bank_transfer_information\",", + " );", " },", " );", "}", @@ -14624,7 +15857,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"klarna_redirect\":{\"issuer_name\":\"stripe\",\"billing_email\":\"arjun.karthik@juspay.in\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"bank_transfer\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_transfer\":{\"ach_bank_transfer\":{\"billing_details\":{\"email\":\"johndoe@example.com\"}}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -14770,7 +16003,7 @@ ] }, { - "name": "Scenario13-BNPL-afterpay", + "name": "Scenario20-Bank Debit-ach", "item": [ { "name": "Payments - Create", @@ -14833,170 +16066,20 @@ " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", " console.log(", " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"SE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"SE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"afterpay_clearpay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'afterpay_clearpay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"afterpay_clearpay\");", - " },", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", " },", " );", "}", @@ -15016,26 +16099,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -15054,27 +16117,18 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"afterpay_clearpay_redirect\":{\"billing_name\":\"Akshaya\",\"billing_email\":\"example@example.com\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"amount\":1800,\"currency\":\"USD\",\"confirm\":true,\"business_label\":\"default\",\"capture_method\":\"automatic\",\"connector\":[\"stripe\"],\"customer_id\":\"klarna\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"authentication_type\":\"three_ds\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"return_url\":\"https://google.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"setup_future_usage\":\"off_session\",\"business_country\":\"US\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"online\",\"accepted_at\":\"2022-09-10T10:11:12Z\",\"online\":{\"ip_address\":\"123.32.25.123\",\"user_agent\":\"Mozilla/5.0 (Linux; Android 12; SM-S906N Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36\"}},\"mandate_type\":{\"single_use\":{\"amount\":6540,\"currency\":\"USD\"}}},\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"billing_details\":{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"},\"account_number\":\"000123456789\",\"routing_number\":\"110000000\"}}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":1800,\"account_name\":\"transaction_processing\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, @@ -15200,7 +16254,7 @@ ] }, { - "name": "Scenario14-BNPL-affirm", + "name": "Scenario21-Wallet-Wechatpay", "item": [ { "name": "Payments - Create", @@ -15305,7 +16359,7 @@ "language": "json" } }, - "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -15402,21 +16456,30 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"next_action.type\"", "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " \"[POST]::/payments - Content check if 'next_action.type' exists\",", " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", + " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", " },", ");", "", - "// Response body should have value \"affirm\" for \"payment_method_type\"", + "// Response body should have value \"ach\" for \"payment_method_type\"", "if (jsonData?.payment_method_type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'affirm'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'we_chat_pay'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"affirm\");", + " pm.expect(jsonData.payment_method_type).to.eql(\"we_chat_pay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"qr_code_information\" for \"next_action.type\"", + "if (jsonData?.next_action.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'qr_code_information'\",", + " function () {", + " pm.expect(jsonData.next_action.type).to.eql(\"qr_code_information\");", " },", " );", "}", @@ -15484,7 +16547,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"affirm_redirect\":{\"issuer_name\":\"affirm\",\"billing_email\":\"user-us@example.com\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"payment_method\":\"wallet\",\"payment_method_type\":\"we_chat_pay\",\"payment_method_data\":{\"wallet\":{\"we_chat_pay_qr\":{}}},\"client_secret\":\"{{client_secret}}\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -15630,7 +16693,7 @@ ] }, { - "name": "Scenario15-Bank Redirect-Ideal", + "name": "Scenario22- Update address and List Payment method", "item": [ { "name": "Payments - Create", @@ -15639,77 +16702,62 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + "})};", "" ], "type": "text/javascript" @@ -15735,7 +16783,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -15751,125 +16799,63 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "List Payment Methods for a Merchant", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + "// Parse the response body as JSON", + "var responseBody = pm.response.json();", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"card\";", + " });", + "});", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"pay_later\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'pay_later'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"pay_later\";", + " });", + "});", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"wallet\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'wallet'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"wallet\";", + " });", + "});", "", - "// Response body should have value \"ideal\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", - " },", - " );", - "}", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_debit\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_debit'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_debit\";", + " });", + "});", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_transfer\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_transfer'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_transfer\";", + " });", + "});" ], "type": "text/javascript" } @@ -15896,126 +16882,84 @@ } ] }, - "method": "POST", + "method": "GET", "header": [ { - "key": "Content-Type", + "key": "Accept", "value": "application/json" }, { - "key": "Accept", - "value": "application/json" + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" + "account", + "payment_methods" ], - "variable": [ + "query": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "client_secret", + "value": "{{client_secret}}" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To filter and list the applicable payment methods for a particular merchant id." }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Update", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + "// Parse the JSON response", + "var jsonData = pm.response.json();", "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "// Check if the 'currency' is equal to \"EUR\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'currency' matches 'EUR' \", function () {", + " pm.expect(jsonData.currency).to.eql(\"EUR\");", + "});", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + "// Extract the \"country\" field from the JSON data", + "var country = jsonData.billing.address.country;", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + "// Check if the country is \"NL\"", + "pm.test(\"[POST]::/payments/:id -Content Check if billing 'Country' matches NL (Netherlands)\", function () {", + " pm.expect(country).to.equal(\"NL\");", + "});", + "", + "var country1 = jsonData.shipping.address.country;", + "", + "// Check if the country is \"NL\"", + "pm.test(\"[POST]::/payments/:id -Content Check if shipping 'Country' matches NL (Netherlands)\", function () {", + " pm.expect(country1).to.equal(\"NL\");", + "});", "" ], "type": "text/javascript" @@ -16023,15 +16967,28 @@ } ], "request": { - "method": "GET", + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"currency\":\"EUR\",\"shipping\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}},\"billing\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}}}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id", "host": [ "{{baseUrl}}" ], @@ -16039,144 +16996,117 @@ "payments", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{payment_id}}" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " }, "response": [] - } - ] - }, - { - "name": "Scenario16-Bank Redirect-sofort", - "item": [ + }, { - "name": "Payments - Create", + "name": "List Payment Methods for a Merchant-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + "// Parse the response body as JSON", + "var responseBody = pm.response.json();", "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"card\";", + " });", + "});", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"ideal\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content Check if payment_method matches 'ideal'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"ideal\";", + " });", + "});", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_redirect\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_redirect'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_redirect\";", + " });", + "});" ], "type": "text/javascript" } } ], "request": { - "method": "POST", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", "header": [ { - "key": "Content-Type", + "key": "Accept", "value": "application/json" }, { - "key": "Accept", - "value": "application/json" + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To filter and list the applicable payment methods for a particular merchant id." }, "response": [] }, @@ -16187,118 +17117,31 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "//// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", - " },", - " );", - "}", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + "})};", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ "" ], "type": "text/javascript" @@ -16344,7 +17187,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_noe_lb_fur_niederosterreich_u_wien\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"Example\",\"email\":\"guest@example.com\"},\"bank_name\":\"ing\"}}},\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"125.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"multi_use\":{\"amount\":7000,\"currency\":\"USD\",\"start_date\":\"2023-04-21T00:00:00Z\",\"end_date\":\"2023-05-21T00:00:00Z\",\"metadata\":{\"frequency\":\"13\"}}}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -16359,8 +17202,7 @@ "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{payment_id}}" } ] }, @@ -16375,78 +17217,55 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", "// Response body should have value \"requires_customer_action\" for \"status\"", "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" + "pm.test(\"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + "})};" ], "type": "text/javascript" } @@ -16490,7 +17309,7 @@ ] }, { - "name": "Scenario17-Bank Redirect-eps", + "name": "Scenario23- Update Amount", "item": [ { "name": "Payments - Create", @@ -16499,77 +17318,62 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + "})};", "" ], "type": "text/javascript" @@ -16595,7 +17399,7 @@ "language": "json" } }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -16611,124 +17415,127 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Update", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + "// Parse the JSON response", + "var jsonData = pm.response.json();", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", "", - "// Response body should have value \"eps\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", - " },", - " );", - "}", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", "" ], "type": "text/javascript" } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":1000}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] }, + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ { - "listen": "prerequest", + "listen": "test", "script": { "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "//// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + "})};", + "", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "//// Response body should have value \"amount_received\" for \"1000\"", + "if (jsonData?.amount_received) {", + "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", + " pm.expect(jsonData.amount_received).to.eql(1000);", + "})};", "" ], "type": "text/javascript" @@ -16765,6 +17572,18 @@ { "key": "Accept", "value": "application/json" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + }, + { + "key": "publishable_key", + "value": "", + "type": "text", + "disabled": true } ], "body": { @@ -16774,7 +17593,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_oberosterreich_salzburg_steiermark\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -16789,8 +17608,7 @@ "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{payment_id}}" } ] }, @@ -16805,77 +17623,67 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "//// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + "})};", + "", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "//// Response body should have value \"amount_received\" for \"1000\"", + "if (jsonData?.amount_received) {", + "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", + " pm.expect(jsonData.amount_received).to.eql(1000);", + "})};", "" ], "type": "text/javascript" @@ -16919,273 +17727,78 @@ } ] }, - { - "name": "Scenario18-Bank Redirect-giropay", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, + { + "name": "Scenario24-Add card flow", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " })};" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -17204,109 +17817,48 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4111111111111111\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "List payment methods for a Customer", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } @@ -17321,116 +17873,126 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" + "customers", + ":customer_id", + "payment_methods" ], "query": [ { - "key": "force_sync", - "value": "true" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true } ], "variable": [ { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] - } - ] - }, - { - "name": "Scenario19-Bank Transfer-ach", - "item": [ + }, { - "name": "Payments - Create", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -17455,7 +18017,7 @@ "language": "json" } }, - "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://hs-payments-test.netlify.app/payments\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -17471,7 +18033,7 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", @@ -17542,45 +18104,16 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.type\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.type' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", "", - "// Response body should have value \"display_bank_transfer_information\" for \"next_action.type\"", - "if (jsonData?.next_action.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'display_bank_transfer_information'\",", - " function () {", - " pm.expect(jsonData.next_action.type).to.eql(", - " \"display_bank_transfer_information\",", - " );", - " },", - " );", - "}", "", "// Response body should have value \"stripe\" for \"connector\"", "if (jsonData?.connector) {", @@ -17595,15 +18128,6 @@ ], "type": "text/javascript" } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } } ], "request": { @@ -17645,7 +18169,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"bank_transfer\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_transfer\":{\"ach_bank_transfer\":{\"billing_details\":{\"email\":\"johndoe@example.com\"}}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -17688,17 +18212,17 @@ " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", @@ -17737,16 +18261,6 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", "" ], "type": "text/javascript" @@ -17762,7 +18276,7 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id", "host": [ "{{baseUrl}}" ], @@ -17770,12 +18284,6 @@ "payments", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", @@ -17787,103 +18295,48 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario19-Bank Debit-ach", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create Copy", "event": [ { "listen": "test", "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", " );", - "}", + "});", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "" ], "type": "text/javascript" } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } } ], "request": { @@ -17905,97 +18358,56 @@ "language": "json" } }, - "raw": "{\"amount\":1800,\"currency\":\"USD\",\"confirm\":true,\"business_label\":\"default\",\"capture_method\":\"automatic\",\"connector\":[\"stripe\"],\"customer_id\":\"klarna\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"authentication_type\":\"three_ds\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"return_url\":\"https://google.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"setup_future_usage\":\"off_session\",\"business_country\":\"US\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"online\",\"accepted_at\":\"2022-09-10T10:11:12Z\",\"online\":{\"ip_address\":\"123.32.25.123\",\"user_agent\":\"Mozilla/5.0 (Linux; Android 12; SM-S906N Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36\"}},\"mandate_type\":{\"single_use\":{\"amount\":6540,\"currency\":\"USD\"}}},\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"billing_details\":{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"},\"account_number\":\"000123456789\",\"routing_number\":\"110000000\"}}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":1800,\"account_name\":\"transaction_processing\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Refunds - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", "// Set response object as internal variable", "let jsonData = {};", "try {", " jsonData = pm.response.json();", "} catch (e) {}", "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", " );", "} else {", " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", " );", "}", "" @@ -18013,36 +18425,30 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], "variable": [ { "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] } ] }, { - "name": "Scenario22-Wallet-Wechatpay", + "name": "Scenario25-Don't Pass CVV for save card flow and verifysuccess payment", "item": [ { "name": "Payments - Create", @@ -18051,272 +18457,262 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx", + "// Validate status 2xx ", "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Validate if response has JSON Body", + "// Validate if response has JSON Body ", "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] }, { - "name": "Payments - Confirm", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", + "try {jsonData = pm.response.json();}catch(e){}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.type\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.type' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'we_chat_pay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"we_chat_pay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"qr_code_information\" for \"next_action.type\"", - "if (jsonData?.next_action.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'qr_code_information'\",", - " function () {", - " pm.expect(jsonData.next_action.type).to.eql(\"qr_code_information\");", - " },", - " );", - "}", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -18335,51 +18731,45 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"wallet\",\"payment_method_type\":\"we_chat_pay\",\"payment_method_data\":{\"wallet\":{\"we_chat_pay_qr\":{}}},\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -18428,43 +18818,76 @@ " );", "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", " },", " );", "}", - "" + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " })};" ], "type": "text/javascript" } } ], "request": { - "method": "GET", + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, { "key": "Accept", "value": "application/json" } ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + }, "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } + ":id", + "confirm" ], "variable": [ { @@ -18474,14 +18897,14 @@ } ] }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] } ] }, { - "name": "Scenario22- Update address and List Payment method", + "name": "Scenario26-Save card payment with manual capture", "item": [ { "name": "Payments - Create", @@ -18534,19 +18957,22 @@ " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", "};", "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", "if (jsonData?.customer_id) {", " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", "} else {", " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", "};", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - "})};", - "" + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" ], "type": "text/javascript" } @@ -18571,7 +18997,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -18587,177 +19013,320 @@ "response": [] }, { - "name": "List Payment Methods for a Merchant", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Parse the response body as JSON", - "var responseBody = pm.response.json();", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"card\";", - " });", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"pay_later\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'pay_later'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"pay_later\";", - " });", - "});", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"wallet\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'wallet'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"wallet\";", - " });", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_debit\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_debit'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_debit\";", - " });", - "});", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_transfer\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_transfer'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_transfer\";", - " });", - "});" + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" ], "type": "text/javascript" } } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "GET", "header": [ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true } ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", "host": [ "{{baseUrl}}" ], "path": [ - "account", + "customers", + ":customer_id", "payment_methods" ], "query": [ { - "key": "client_secret", - "value": "{{client_secret}}" + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" } ] }, - "description": "To filter and list the applicable payment methods for a particular merchant id." + "description": "To filter and list the applicable payment methods for a particular Customer ID" }, "response": [] }, { - "name": "Payments - Update", + "name": "Save card payments - Create", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", "});", "", "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", "", - "// Parse the JSON response", - "var jsonData = pm.response.json();", - "", - "// Check if the 'currency' is equal to \"EUR\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'currency' matches 'EUR' \", function () {", - " pm.expect(jsonData.currency).to.eql(\"EUR\");", - "});", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", "", - "// Extract the \"country\" field from the JSON data", - "var country = jsonData.billing.address.country;", "", - "// Check if the country is \"NL\"", - "pm.test(\"[POST]::/payments/:id -Content Check if billing 'Country' matches NL (Netherlands)\", function () {", - " pm.expect(country).to.equal(\"NL\");", - "});", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", "", - "var country1 = jsonData.shipping.address.country;", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", "", - "// Check if the country is \"NL\"", - "pm.test(\"[POST]::/payments/:id -Content Check if shipping 'Country' matches NL (Netherlands)\", function () {", - " pm.expect(country1).to.equal(\"NL\");", - "});", - "" + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" ], "type": "text/javascript" } @@ -18782,72 +19351,114 @@ "language": "json" } }, - "raw": "{\"currency\":\"EUR\",\"shipping\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}},\"billing\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } + "payments" ] }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, { - "name": "List Payment Methods for a Merchant-copy", + "name": "Save card payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", - "// Parse the response body as JSON", - "var responseBody = pm.response.json();", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"card\";", - " });", - "});", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"ideal\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content Check if payment_method matches 'ideal'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"ideal\";", - " });", - "});", "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_redirect\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_redirect'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_redirect\";", - " });", - "});" + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -18874,80 +19485,164 @@ } ] }, - "method": "GET", + "method": "POST", "header": [ { - "key": "Accept", + "key": "Content-Type", "value": "application/json" }, { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true + "key": "Accept", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "", "options": { "raw": { "language": "json" } - } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" }, "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ - "account", - "payment_methods" + "payments", + ":id", + "confirm" ], - "query": [ + "variable": [ { - "key": "client_secret", - "value": "{{client_secret}}" + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "To filter and list the applicable payment methods for a particular merchant id." + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", - "//// Response body should have value \"requires_customer_action\" for \"status\"", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - "})};", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -18955,26 +19650,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -18993,85 +19668,144 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"Example\",\"email\":\"guest@example.com\"},\"bank_name\":\"ing\"}}},\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"125.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"multi_use\":{\"amount\":7000,\"currency\":\"USD\",\"start_date\":\"2023-04-21T00:00:00Z\",\"end_date\":\"2023-05-21T00:00:00Z\",\"metadata\":{\"frequency\":\"13\"}}}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}" + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "confirm" + "capture" ], "variable": [ { "key": "id", - "value": "{{payment_id}}" + "value": "{{payment_id}}", + "description": "(Required) unique payment id" } ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To capture the funds for an uncaptured payment" }, "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Retrieve-copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "// Response body should have value \"requires_customer_action\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - "})};" + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" ], "type": "text/javascript" } @@ -19111,75 +19845,69 @@ "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - } - ] - }, - { - "name": "Scenario23- Update Amount", - "item": [ + }, { - "name": "Payments - Create", + "name": "Refunds - Create Copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"540\" for \"amount\"", "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - "})};", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", "" ], "type": "text/javascript" @@ -19205,53 +19933,78 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] }, { - "name": "Payments - Update", + "name": "Refunds - Retrieve Copy", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// Parse the JSON response", - "var jsonData = pm.response.json();", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", "" ], "type": "text/javascript" @@ -19259,89 +20012,126 @@ } ], "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, + "method": "GET", + "header": [ { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000}" - }, "url": { - "raw": "{{baseUrl}}/payments/:id", + "raw": "{{baseUrl}}/refunds/:id", "host": [ "{{baseUrl}}" ], "path": [ - "payments", + "refunds", ":id" ], "variable": [ { "key": "id", - "value": "{{payment_id}}" + "value": "{{refund_id}}", + "description": "(Required) unique refund id" } ] }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "item": [ { - "name": "Payments - Confirm", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", - "//// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - "})};", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "", - "//// Response body should have value \"amount_received\" for \"1000\"", - "if (jsonData?.amount_received) {", - "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", - " pm.expect(jsonData.amount_received).to.eql(1000);", - "})};", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -19349,26 +20139,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -19378,18 +20148,6 @@ { "key": "Accept", "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - }, - { - "key": "publishable_key", - "value": "", - "type": "text", - "disabled": true } ], "body": { @@ -19399,26 +20157,18 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] }, @@ -19429,67 +20179,86 @@ "listen": "test", "script": { "exec": [ - "// Validate status 2xx ", + "// Validate status 2xx", "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", "});", "", - "// Validate if response has JSON Body ", + "// Validate if response has JSON Body", "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", + " pm.response.to.have.jsonBody();", "});", "", "// Set response object as internal variable", "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", "", "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", "", "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", "", - "//// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - "})};", - "", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", "", - "//// Response body should have value \"amount_received\" for \"1000\"", - "if (jsonData?.amount_received) {", - "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", - " pm.expect(jsonData.amount_received).to.eql(1000);", - "})};", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" diff --git a/postman/collection-json/trustpay.postman_collection.json b/postman/collection-json/trustpay.postman_collection.json index b88fa9e110c9..37c7fecf7ec9 100644 --- a/postman/collection-json/trustpay.postman_collection.json +++ b/postman/collection-json/trustpay.postman_collection.json @@ -811,7 +811,7 @@ "name": "Happy Cases", "item": [ { - "name": "Scenario1-Create payment with confirm true", + "name": "Scenario2a-Create payment with confirm false card holder name empty", "item": [ { "name": "Payments - Create", @@ -882,12 +882,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -916,7 +916,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -931,6 +931,164 @@ }, "response": [] }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, { "name": "Payments - Retrieve", "event": [ @@ -1000,12 +1158,12 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -1053,7 +1211,7 @@ ] }, { - "name": "Scenario2-Create payment with confirm false", + "name": "Scenario2b-Create payment with confirm false card holder name null", "item": [ { "name": "Payments - Create", @@ -1124,12 +1282,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -1158,7 +1316,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -1299,7 +1457,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -2303,6 +2461,15 @@ ], "type": "text/javascript" } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } } ], "request": { @@ -2344,7 +2511,7 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"return_url\":\"https://integ.hyperswitch.io/home\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"ip_address\":\"65.1.52.138\",\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true}}" + "raw": "{\"client_secret\":\"{{client_secret}}\",\"return_url\":\"https://integ.hyperswitch.io/home\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"John Doe\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"ip_address\":\"65.1.52.138\",\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true}}" }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", @@ -2922,7 +3089,7 @@ ] }, { - "name": "Scenario8-Bank Redirect-Ideal", + "name": "Scenario7-Bank Redirect-Ideal", "item": [ { "name": "Payments - Create", @@ -3344,7 +3511,7 @@ ] }, { - "name": "Scenario11-Bank Redirect-giropay", + "name": "Scenario8-Bank Redirect-giropay", "item": [ { "name": "Payments - Create", @@ -3764,25 +3931,20 @@ "response": [] } ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ + }, { - "name": "Scenario1-Create payment with Invalid card details", + "name": "Scenario1-Create payment with confirm true", "item": [ { - "name": "Payments - Create(Invalid card number)", + "name": "Payments - Create", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", @@ -3842,17 +4004,12 @@ " );", "}", "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3881,7 +4038,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"12345\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3897,26 +4054,26 @@ "response": [] }, { - "name": "Payments - Create(Invalid Exp month)", + "name": "Payments - Retrieve", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", " );", "});", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -3965,17 +4122,12 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3986,41 +4138,799 @@ } ], "request": { - "method": "POST", + "method": "GET", "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, { "key": "Accept", "value": "application/json" } ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario5-Refund for unsuccessful payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"19\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments", + "raw": "{{baseUrl}}/refunds", "host": [ "{{baseUrl}}" ], "path": [ - "payments" + "refunds" ] }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + "description": "To create a refund against an already processed payment" }, "response": [] - }, + } + ] + }, + { + "name": "Scenario1-Create payment with Invalid card details", + "item": [ { - "name": "Payments - Create(Invalid", + "name": "Payments - Create(Invalid card number)", "event": [ { "listen": "test", @@ -4088,7 +4998,7 @@ " );", "}", "", - "// Response body should have \"next_action.redirect_to_url\"", + "// Response body should have \"error\"", "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", "});", @@ -4096,9 +5006,9 @@ "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", " },", " );", "}", @@ -4127,7 +5037,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"12345\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4143,7 +5053,7 @@ "response": [] }, { - "name": "Payments - Create(invalid CVV)", + "name": "Payments - Create(Invalid Exp month)", "event": [ { "listen": "test", @@ -4211,7 +5121,7 @@ " );", "}", "", - "// Response body should have \"error\"", + "// Response body should have \"next_action.redirect_to_url\"", "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", "});", @@ -4250,7 +5160,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"19\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4264,22 +5174,17 @@ "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] - } - ] - }, - { - "name": "Scenario2-Confirming the payment without PMD", - "item": [ + }, { - "name": "Payments - Create", + "name": "Payments - Create(Invalid", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", "});", "", "// Validate if response header has matching content-type", @@ -4339,12 +5244,17 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", " },", " );", "}", @@ -4373,7 +5283,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4389,7 +5299,7 @@ "response": [] }, { - "name": "Payments - Confirm", + "name": "Payments - Create(invalid CVV)", "event": [ { "listen": "test", @@ -4400,18 +5310,15 @@ " pm.response.to.be.error;", "});", "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4461,17 +5368,14 @@ "}", "", "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", "", "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", " function () {", " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", " },", @@ -4484,26 +5388,6 @@ } ], "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, "method": "POST", "header": [ { @@ -4522,34 +5406,25 @@ "language": "json" } }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", + "raw": "{{baseUrl}}/payments", "host": [ "{{baseUrl}}" ], "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } + "payments" ] }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" }, "response": [] } ] }, { - "name": "Scenario3-Capture the succeeded payment", + "name": "Scenario2-Confirming the payment without PMD", "item": [ { "name": "Payments - Create", @@ -4620,12 +5495,12 @@ " );", "}", "", - "// Response body should have value \"succeeded\" for \"status\"", + "// Response body should have value \"requires_payment_method\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", " },", " );", "}", @@ -4654,7 +5529,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -4670,20 +5545,20 @@ "response": [] }, { - "name": "Payments - Capture", + "name": "Payments - Confirm", "event": [ { "listen": "test", "script": { "exec": [ "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", " pm.response.to.be.error;", "});", "", "// Validate if response header has matching content-type", "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", " function () {", " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", " \"application/json\",", @@ -4692,7 +5567,7 @@ ");", "", "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4765,6 +5640,26 @@ } ], "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, "method": "POST", "header": [ { @@ -4783,17 +5678,17 @@ "language": "json" } }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + "raw": "{\"client_secret\":\"{{client_secret}}\"}" }, "url": { - "raw": "{{baseUrl}}/payments/:id/capture", + "raw": "{{baseUrl}}/payments/:id/confirm", "host": [ "{{baseUrl}}" ], "path": [ "payments", ":id", - "capture" + "confirm" ], "variable": [ { @@ -4803,14 +5698,14 @@ } ] }, - "description": "To capture the funds for an uncaptured payment" + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" }, "response": [] } ] }, { - "name": "Scenario4-Refund exceeds amount", + "name": "Scenario3-Capture the succeeded payment", "item": [ { "name": "Payments - Create", @@ -4931,26 +5826,29 @@ "response": [] }, { - "name": "Payments - Retrieve", + "name": "Payments - Capture", "event": [ { "listen": "test", "script": { "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", "});", "", "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", "", "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", " pm.response.to.have.jsonBody();", "});", "", @@ -4999,94 +5897,6 @@ " );", "}", "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", "// Response body should have \"error\"", "pm.test(", " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", @@ -5129,25 +5939,34 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" }, "url": { - "raw": "{{baseUrl}}/refunds", + "raw": "{{baseUrl}}/payments/:id/capture", "host": [ "{{baseUrl}}" ], "path": [ - "refunds" + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } ] }, - "description": "To create a refund against an already processed payment" + "description": "To capture the funds for an uncaptured payment" }, "response": [] } ] }, { - "name": "Scenario8-Refund for unsuccessful payment", + "name": "Scenario4-Refund exceeds amount", "item": [ { "name": "Payments - Create", @@ -5218,12 +6037,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -5252,7 +6071,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -5336,12 +6155,12 @@ " );", "}", "", - "// Response body should have value \"requires_confirmation\" for \"status\"", + "// Response body should have value \"Succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -5432,7 +6251,7 @@ " },", ");", "", - "// Response body should have value \"invalid_request\" for \"error type\"", + "// Response body should have value \"connector error\" for \"error type\"", "if (jsonData?.error?.type) {", " pm.test(", " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", @@ -5466,7 +6285,7 @@ "language": "json" } }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" }, "url": { "raw": "{{baseUrl}}/refunds", From ca2c399dc903f84468580e90f71129f5fcfc2cd3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:52:17 +0000 Subject: [PATCH 183/443] chore(version): v1.99.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ed03591eaa..d8d604bc8007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.99.0 (2023-12-12) + +### Features + +- **connector:** [Placetopay] Add Connector Template Code ([#3084](https://github.com/juspay/hyperswitch/pull/3084)) ([`a7b688a`](https://github.com/juspay/hyperswitch/commit/a7b688aac72e15f782046b9d108aca12f43a9994)) +- Add utility to convert TOML configuration file to list of environment variables ([#3096](https://github.com/juspay/hyperswitch/pull/3096)) ([`2c4599a`](https://github.com/juspay/hyperswitch/commit/2c4599a1cd7e244b6fb11948c88c55c5b8faad76)) + +### Bug Fixes + +- **router:** Make `request_incremental_authorization` optional in payment_intent ([#3086](https://github.com/juspay/hyperswitch/pull/3086)) ([`f7da59d`](https://github.com/juspay/hyperswitch/commit/f7da59d06af11707e210b58a875c013d31c3ee17)) + +### Refactors + +- **email:** Create client every time of sending email ([#3105](https://github.com/juspay/hyperswitch/pull/3105)) ([`fc2f163`](https://github.com/juspay/hyperswitch/commit/fc2f16392148cd66b3c3e67e3e0c782910e37e1f)) + +### Testing + +- **postman:** Update postman collection files ([`aa97821`](https://github.com/juspay/hyperswitch/commit/aa9782164fb7846fe533c5057a17756dc82ede54)) + +### Miscellaneous Tasks + +- **deps:** Update fred and moka ([#3088](https://github.com/juspay/hyperswitch/pull/3088)) ([`129b1e5`](https://github.com/juspay/hyperswitch/commit/129b1e55bd1cbad0243030fd25379f1400eb170c)) + +**Full Changelog:** [`v1.98.0...v1.99.0`](https://github.com/juspay/hyperswitch/compare/v1.98.0...v1.99.0) + +- - - + + ## 1.98.0 (2023-12-11) ### Features From 62a7c3053c5e276091f5bd54a5679caef58a4ace Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:43:53 +0530 Subject: [PATCH 184/443] fix(connector): [trustpay] make paymentId optional field (#3101) --- crates/router/src/connector/trustpay.rs | 6 +++++- crates/router/src/connector/trustpay/transformers.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 286eaf9cb542..9d9d998aa18c 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -955,11 +955,15 @@ impl api::IncomingWebhook for Trustpay { .switch()?; let payment_info = trustpay_response.payment_information; let reason = payment_info.status_reason_information.unwrap_or_default(); + let connector_dispute_id = payment_info + .references + .payment_id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?; Ok(api::disputes::DisputePayload { amount: payment_info.amount.amount.to_string(), currency: payment_info.amount.currency, dispute_stage: api_models::enums::DisputeStage::Dispute, - connector_dispute_id: payment_info.references.payment_id, + connector_dispute_id, connector_reason: reason.reason.reject_reason, connector_reason_code: Some(reason.reason.code), challenge_required_by: None, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 270a702bd6ec..c112b6440178 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -1700,7 +1700,7 @@ impl TryFrom for diesel_models::enums::RefundStatus { #[serde(rename_all = "PascalCase")] pub struct WebhookReferences { pub merchant_reference: String, - pub payment_id: String, + pub payment_id: Option, pub payment_request_id: Option, } From 151a30f4eed10924cd93bf7f4f66976af0ab8314 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:39:36 +0530 Subject: [PATCH 185/443] feat(connector): [RISKIFIED] Add support for riskified frm connector (#2533) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Jagan --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/api_models/src/enums.rs | 3 + crates/api_models/src/payments.rs | 35 + crates/common_enums/src/enums.rs | 1 + crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 8 +- crates/router/src/connector/riskified.rs | 557 ++++++++++++++++ .../src/connector/riskified/transformers.rs | 7 + .../connector/riskified/transformers/api.rs | 597 ++++++++++++++++++ .../connector/riskified/transformers/auth.rs | 22 + crates/router/src/connector/signifyd.rs | 4 +- .../connector/signifyd/transformers/api.rs | 33 +- crates/router/src/connector/utils.rs | 132 +++- crates/router/src/core/admin.rs | 7 +- crates/router/src/core/fraud_check.rs | 19 +- .../core/fraud_check/flows/checkout_flow.rs | 29 +- .../fraud_check/flows/fulfillment_flow.rs | 7 +- .../core/fraud_check/flows/record_return.rs | 1 + .../src/core/fraud_check/flows/sale_flow.rs | 1 + .../fraud_check/flows/transaction_flow.rs | 4 + .../fraud_check/operation/fraud_check_post.rs | 7 +- .../fraud_check/operation/fraud_check_pre.rs | 13 +- crates/router/src/core/fraud_check/types.rs | 10 + crates/router/src/core/payments.rs | 1 + crates/router/src/core/payments/flows.rs | 20 + crates/router/src/core/payments/helpers.rs | 1 + .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + .../operations/payment_method_validate.rs | 1 + .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../payments_incremental_authorization.rs | 1 + .../router/src/core/payments/transformers.rs | 6 + crates/router/src/core/utils.rs | 7 + crates/router/src/core/webhooks/utils.rs | 1 + crates/router/src/openapi.rs | 1 + crates/router/src/types.rs | 4 + crates/router/src/types/api.rs | 4 +- crates/router/src/types/api/fraud_check.rs | 1 + .../router/src/types/api/verify_connector.rs | 1 + crates/router/src/types/fraud_check.rs | 18 +- crates/router/src/types/transformers.rs | 6 + crates/router/tests/connectors/aci.rs | 2 + crates/router/tests/connectors/payme.rs | 20 + crates/router/tests/connectors/utils.rs | 1 + crates/router/tests/connectors/zen.rs | 20 + crates/router_env/Cargo.toml | 2 +- loadtest/config/development.toml | 1 + openapi/openapi_spec.json | 76 ++- 58 files changed, 1646 insertions(+), 61 deletions(-) create mode 100644 crates/router/src/connector/riskified.rs create mode 100644 crates/router/src/connector/riskified/transformers.rs create mode 100644 crates/router/src/connector/riskified/transformers/api.rs create mode 100644 crates/router/src/connector/riskified/transformers/auth.rs diff --git a/config/config.example.toml b/config/config.example.toml index 1b720eaeb42c..05fdb8827632 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -220,6 +220,7 @@ placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" diff --git a/config/development.toml b/config/development.toml index 1ce26053fc77..0b7b9756b477 100644 --- a/config/development.toml +++ b/config/development.toml @@ -207,6 +207,7 @@ placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 00840daf55fe..35c97e6b5967 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -142,6 +142,7 @@ placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index f1b4447c3316..558223a68eed 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -127,6 +127,7 @@ pub enum Connector { Zen, Signifyd, Plaid, + Riskified, } impl Connector { @@ -200,6 +201,7 @@ impl From for RoutableConnectors { pub enum FrmConnectors { /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/ Signifyd, + Riskified, } #[cfg(feature = "frm")] @@ -207,6 +209,7 @@ impl From for RoutableConnectors { fn from(value: FrmConnectors) -> Self { match value { FrmConnectors::Signifyd => Self::Signifyd, + FrmConnectors::Riskified => Self::Riskified, } } } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b19f4d7b7db1..ef0ae3a15ce6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -313,6 +313,9 @@ pub struct PaymentsRequest { ///Request for an incremental authorization pub request_incremental_authorization: Option, + + /// additional data related to some frm connectors + pub frm_metadata: Option, } impl PaymentsRequest { @@ -2598,8 +2601,30 @@ pub struct OrderDetailsWithAmount { pub quantity: u16, /// the amount per quantity of product pub amount: i64, + // Does the order includes shipping + pub requires_shipping: Option, /// The image URL of the product pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, +} + +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProductType { + #[default] + Physical, + Digital, + Travel, + Ride, + Event, + Accommodation, } #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -2610,8 +2635,18 @@ pub struct OrderDetails { /// The quantity of the product to be purchased #[schema(example = 1)] pub quantity: u16, + // Does the order include shipping + pub requires_shipping: Option, /// The image URL of the product pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, } #[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index a749744e06df..0c4b9720cab8 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -146,6 +146,7 @@ pub enum RoutableConnectors { Powertranz, Prophetpay, Rapyd, + Riskified, Shift4, Signifyd, Square, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 5b34d56c9388..f73fe099c295 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -618,6 +618,7 @@ pub struct Connectors { pub powertranz: ConnectorParams, pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, + pub riskified: ConnectorParams, pub shift4: ConnectorParams, pub signifyd: ConnectorParams, pub square: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index e336d8e3514f..de6e250842c7 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -40,6 +40,7 @@ pub mod placetopay; pub mod powertranz; pub mod prophetpay; pub mod rapyd; +pub mod riskified; pub mod shift4; pub mod signifyd; pub mod square; @@ -65,7 +66,8 @@ pub use self::{ iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, placetopay::Placetopay, - powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, - signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, - volt::Volt, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, + powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, + shift4::Shift4, signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, + trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, + worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs new file mode 100644 index 000000000000..e34d12def02a --- /dev/null +++ b/crates/router/src/connector/riskified.rs @@ -0,0 +1,557 @@ +pub mod transformers; +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface}; +use ring::hmac; +use transformers as riskified; + +#[cfg(feature = "frm")] +use super::utils::FrmTransactionRouterDataRequest; +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + }, +}; +#[cfg(feature = "frm")] +use crate::{ + services, + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Riskified; + +impl Riskified { + pub fn generate_authorization_signature( + &self, + auth: &riskified::RiskifiedAuthType, + payload: &str, + ) -> CustomResult { + let key = hmac::Key::new( + hmac::HMAC_SHA256, + auth.secret_token.clone().expose().as_bytes(), + ); + + let signature_value = hmac::sign(&key, payload.as_bytes()); + + let digest = signature_value.as_ref(); + + Ok(hex::encode(digest)) + } +} + +impl ConnectorCommonExt for Riskified +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let auth: riskified::RiskifiedAuthType = + riskified::RiskifiedAuthType::try_from(&req.connector_auth_type)?; + + let riskified_req = self + .get_request_body(req, connectors)? + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + + let binding = types::RequestBody::get_inner_value(riskified_req); + let payload = binding.peek(); + + let digest = self + .generate_authorization_signature(&auth, payload) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + "X-RISKIFIED-SHOP-DOMAIN".to_string(), + auth.domain_name.clone().into(), + ), + ( + "X-RISKIFIED-HMAC-SHA256".to_string(), + request::Mask::into_masked(digest), + ), + ( + "Accept".to_string(), + "application/vnd.riskified.com; version=2".into(), + ), + ]; + + Ok(header) + } +} + +impl ConnectorCommon for Riskified { + fn id(&self) -> &'static str { + "riskified" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.riskified.base_url.as_ref() + } + + #[cfg(feature = "frm")] + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: riskified::ErrorResponse = res + .response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + attempt_status: None, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.error.message.clone(), + reason: None, + connector_transaction_id: None, + }) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + fn get_headers( + &self, + req: &frm_types::FrmCheckoutRouterData, + 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: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/decide")) + } + + fn get_request_body( + &self, + req: &frm_types::FrmCheckoutRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = riskified::RiskifiedPaymentsCheckoutRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmCheckoutType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmCheckoutType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmCheckoutType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmCheckoutRouterData, + res: Response, + ) -> CustomResult { + let response: riskified::RiskifiedPaymentsResponse = res + .response + .parse_struct("RiskifiedPaymentsResponse Checkout") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Payment for Riskified {} +impl api::PaymentAuthorize for Riskified {} +impl api::PaymentSync for Riskified {} +impl api::PaymentVoid for Riskified {} +impl api::PaymentCapture for Riskified {} +impl api::MandateSetup for Riskified {} +impl api::ConnectorAccessToken for Riskified {} +impl api::PaymentToken for Riskified {} +impl api::Refund for Riskified {} +impl api::RefundExecute for Riskified {} +impl api::RefundSync for Riskified {} +impl ConnectorValidation for Riskified {} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for Riskified +{ +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + fn get_headers( + &self, + req: &frm_types::FrmTransactionRouterData, + 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: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + match req.is_payment_successful() { + Some(false) => Ok(format!( + "{}{}", + self.base_url(connectors), + "/checkout_denied" + )), + Some(true) => Ok(format!("{}{}", self.base_url(connectors), "/decision")), + None => Err(errors::ConnectorError::FlowNotSupported { + flow: "Transaction".to_owned(), + connector: req.connector.to_string(), + })?, + } + } + + fn get_request_body( + &self, + req: &frm_types::FrmTransactionRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + match req.is_payment_successful() { + Some(false) => { + let req_obj = riskified::TransactionFailedRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_req)) + } + Some(true) => { + let req_obj = riskified::TransactionSuccessRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_req)) + } + None => Err(errors::ConnectorError::FlowNotSupported { + flow: "Transaction".to_owned(), + connector: req.connector.to_owned(), + })?, + } + } + + fn build_request( + &self, + req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmTransactionType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmTransactionType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmTransactionType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmTransactionRouterData, + res: Response, + ) -> CustomResult { + let response: riskified::RiskifiedTransactionResponse = res + .response + .parse_struct("RiskifiedPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + match response { + riskified::RiskifiedTransactionResponse::FailedResponse(response_data) => { + ::try_from(types::ResponseRouterData { + response: response_data, + data: data.clone(), + http_code: res.status_code, + }) + } + riskified::RiskifiedTransactionResponse::SuccessResponse(response_data) => { + ::try_from(types::ResponseRouterData { + response: response_data, + data: data.clone(), + http_code: res.status_code, + }) + } + } + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + fn get_headers( + &self, + req: &frm_types::FrmFulfillmentRouterData, + 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: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "/fulfill")) + } + + fn get_request_body( + &self, + req: &frm_types::FrmFulfillmentRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = riskified::RiskifiedFullfillmentRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmFulfillmentType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmFulfillmentType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmFulfillmentType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmFulfillmentRouterData, + res: Response, + ) -> CustomResult { + let response: riskified::RiskifiedFulfilmentResponse = res + .response + .parse_struct("RiskifiedFulfilmentResponse fulfilment") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + frm_types::FrmFulfillmentRouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + frm_api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for Riskified +{ +} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Riskified +{ +} + +impl api::PaymentSession for Riskified {} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +#[cfg(feature = "frm")] +impl api::FraudCheck for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckSale for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckCheckout for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckTransaction for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckFulfillment for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckRecordReturn for Riskified {} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Riskified { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/riskified/transformers.rs b/crates/router/src/connector/riskified/transformers.rs new file mode 100644 index 000000000000..4f155f341f6d --- /dev/null +++ b/crates/router/src/connector/riskified/transformers.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "frm")] +pub mod api; +pub mod auth; + +#[cfg(feature = "frm")] +pub use self::api::*; +pub use self::auth::*; diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs new file mode 100644 index 000000000000..de8884f03909 --- /dev/null +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -0,0 +1,597 @@ +use api_models::payments::AdditionalPaymentData; +use common_utils::{ext_traits::ValueExt, pii::Email}; +use error_stack::{self, ResultExt}; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckTransactionRequest, RouterData, + }, + core::{errors, fraud_check::types as core_types}, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +type Error = error_stack::Report; + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedPaymentsCheckoutRequest { + order: CheckoutRequest, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct CheckoutRequest { + id: String, + note: Option, + email: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + currency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + updated_at: PrimitiveDateTime, + gateway: Option, + browser_ip: Option, + total_price: i64, + total_discounts: i64, + cart_token: String, + referring_site: String, + line_items: Vec, + discount_codes: Vec, + shipping_lines: Vec, + payment_details: Option, + customer: RiskifiedCustomer, + billing_address: Option, + shipping_address: Option, + source: Source, + client_details: ClientDetails, + vendor_name: String, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct PaymentDetails { + credit_card_bin: Option>, + credit_card_number: Option>, + credit_card_company: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ShippingLines { + price: i64, + title: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct DiscountCodes { + amount: i64, + code: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ClientDetails { + user_agent: Option, + accept_language: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedCustomer { + email: Option, + first_name: Option>, + last_name: Option>, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + verified_email: bool, + id: String, + account_type: CustomerAccountType, + orders_count: i32, + phone: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum CustomerAccountType { + Guest, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderAddress { + first_name: Option>, + last_name: Option>, + address1: Option>, + country_code: Option, + city: Option, + province: Option>, + phone: Option>, + zip: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct LineItem { + price: i64, + quantity: i32, + title: String, + product_type: Option, + requires_shipping: Option, + product_id: Option, + category: Option, + brand: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Source { + DesktopWeb, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedMetadata { + vendor_name: String, + shipping_lines: Vec, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutRequest { + type Error = Error; + fn try_from(payment_data: &frm_types::FrmCheckoutRouterData) -> Result { + let metadata: RiskifiedMetadata = payment_data + .frm_metadata + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "frm_metadata", + })? + .parse_value("Riskified Metadata") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing_address = payment_data.get_billing_address_with_phone_number()?; + let shipping_address = payment_data.get_shipping_address_with_phone_number()?; + let address = payment_data.get_billing_address()?; + + Ok(Self { + order: CheckoutRequest { + id: payment_data.attempt_id.clone(), + email: payment_data.request.email.clone(), + created_at: common_utils::date_time::now(), + updated_at: common_utils::date_time::now(), + gateway: payment_data.request.gateway.clone(), + total_price: payment_data.request.amount, + cart_token: payment_data.attempt_id.clone(), + line_items: payment_data + .request + .get_order_details()? + .iter() + .map(|order_detail| LineItem { + price: order_detail.amount, + quantity: i32::from(order_detail.quantity), + title: order_detail.product_name.clone(), + product_type: order_detail.product_type.clone(), + requires_shipping: order_detail.requires_shipping, + product_id: order_detail.product_id.clone(), + category: order_detail.category.clone(), + brand: order_detail.brand.clone(), + }) + .collect::>(), + source: Source::DesktopWeb, + billing_address: OrderAddress::try_from(billing_address).ok(), + shipping_address: OrderAddress::try_from(shipping_address).ok(), + total_discounts: 0, + currency: payment_data.request.currency, + referring_site: "hyperswitch.io".to_owned(), + discount_codes: Vec::new(), + shipping_lines: metadata.shipping_lines, + customer: RiskifiedCustomer { + email: payment_data.request.email.clone(), + + first_name: address.get_first_name().ok().cloned(), + last_name: address.get_last_name().ok().cloned(), + created_at: common_utils::date_time::now(), + verified_email: false, + id: payment_data.get_customer_id()?, + account_type: CustomerAccountType::Guest, + orders_count: 0, + phone: billing_address + .clone() + .phone + .and_then(|phone_data| phone_data.number), + }, + browser_ip: payment_data + .request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.ip_address), + client_details: ClientDetails { + user_agent: payment_data + .request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.user_agent.clone()), + accept_language: payment_data.request.browser_info.as_ref().and_then( + |browser_info: &types::BrowserInformation| browser_info.language.clone(), + ), + }, + note: payment_data.description.clone(), + vendor_name: metadata.vendor_name, + payment_details: match payment_data.request.payment_method_data.as_ref() { + Some(AdditionalPaymentData::Card(card_info)) => Some(PaymentDetails { + credit_card_bin: card_info.card_isin.clone().map(Secret::new), + credit_card_number: card_info + .last4 + .clone() + .map(|last_four| format!("XXXX-XXXX-XXXX-{}", last_four)) + .map(Secret::new), + credit_card_company: card_info.card_network.clone(), + }), + Some(_) | None => None, + }, + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedPaymentsResponse { + order: OrderResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderResponse { + id: String, + status: PaymentStatus, + description: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFulfilmentResponse { + order: OrderFulfilmentResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderFulfilmentResponse { + id: String, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FulfilmentStatus { + Fulfilled, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PaymentStatus { + Captured, + Created, + Submitted, + Approved, + Declined, + Processing, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + F, + RiskifiedPaymentsResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order.id), + status: storage_enums::FraudCheckStatus::from(item.response.order.status), + connector_metadata: None, + score: None, + reason: item.response.order.description.map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: PaymentStatus) -> Self { + match item { + PaymentStatus::Approved => Self::Legit, + PaymentStatus::Declined => Self::Fraud, + _ => Self::Pending, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionFailedRequest { + checkout: FailedTransactionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct FailedTransactionData { + id: String, + payment_details: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct DeclinedPaymentDetails { + authorization_error: AuthorizationError, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct AuthorizationError { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + error_code: Option, + message: Option, +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionFailedRequest { + type Error = Error; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + Ok(Self { + checkout: FailedTransactionData { + id: item.attempt_id.clone(), + payment_details: [DeclinedPaymentDetails { + authorization_error: AuthorizationError { + created_at: common_utils::date_time::now(), + error_code: item.request.error_code.clone(), + message: item.request.error_message.clone(), + }, + }] + .to_vec(), + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFailedTransactionResponse { + checkout: OrderResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum RiskifiedTransactionResponse { + FailedResponse(RiskifiedFailedTransactionResponse), + SuccessResponse(RiskifiedPaymentsResponse), +} + +impl + TryFrom< + ResponseRouterData< + F, + RiskifiedFailedTransactionResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + F, + RiskifiedFailedTransactionResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.checkout.id), + status: storage_enums::FraudCheckStatus::from(item.response.checkout.status), + connector_metadata: None, + score: None, + reason: item + .response + .checkout + .description + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionSuccessRequest { + order: SuccessfulTransactionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct SuccessfulTransactionData { + id: String, + decision: TransactionDecisionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionDecisionData { + external_status: TransactionStatus, + reason: Option, + amount: i64, + currency: storage_enums::Currency, + #[serde(with = "common_utils::custom_serde::iso8601")] + decided_at: PrimitiveDateTime, + payment_details: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionPaymentDetails { + authorization_id: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum TransactionStatus { + Approved, +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionSuccessRequest { + type Error = Error; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + Ok(Self { + order: SuccessfulTransactionData { + id: item.attempt_id.clone(), + decision: TransactionDecisionData { + external_status: TransactionStatus::Approved, + reason: None, + amount: item.request.amount, + currency: item.request.get_currency()?, + decided_at: common_utils::date_time::now(), + payment_details: [TransactionPaymentDetails { + authorization_id: item.request.connector_transaction_id.clone(), + }] + .to_vec(), + }, + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFullfillmentRequest { + order: OrderFullfillment, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FulfillmentRequestStatus { + Success, + Cancelled, + Error, + Failure, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderFullfillment { + id: String, + fulfillments: FulfilmentData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct FulfilmentData { + fulfillment_id: String, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + status: Option, + tracking_company: String, + tracking_number: String, + tracking_url: Option, +} + +impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequest { + type Error = Error; + fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { + Ok(Self { + order: OrderFullfillment { + id: item.attempt_id.clone(), + fulfillments: FulfilmentData { + fulfillment_id: item.payment_id.clone(), + created_at: common_utils::date_time::now(), + status: item + .request + .fulfillment_req + .fulfillment_status + .clone() + .and_then(get_fulfillment_status), + tracking_company: item + .request + .fulfillment_req + .tracking_company + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "tracking_company", + })?, + tracking_number: item.request.fulfillment_req.tracking_number.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "tracking_number", + }, + )?, + tracking_url: item.request.fulfillment_req.tracking_url.clone(), + }, + }, + }) + } +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + RiskifiedFulfilmentResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + > + for types::RouterData< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > +{ + type Error = Error; + fn try_from( + item: ResponseRouterData< + Fulfillment, + RiskifiedFulfilmentResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order.id, + shipment_ids: Vec::new(), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ErrorResponse { + pub error: ErrorData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ErrorData { + pub message: String, +} + +impl TryFrom<&api_models::payments::Address> for OrderAddress { + type Error = Error; + fn try_from(address_info: &api_models::payments::Address) -> Result { + let address = + address_info + .clone() + .address + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "address", + })?; + Ok(Self { + first_name: address.first_name.clone(), + last_name: address.last_name.clone(), + address1: address.line1.clone(), + country_code: address.country, + city: address.city.clone(), + province: address.state.clone(), + zip: address.zip.clone(), + phone: address_info + .phone + .clone() + .and_then(|phone_data| phone_data.number), + }) + } +} + +fn get_fulfillment_status( + status: core_types::FulfillmentStatus, +) -> Option { + match status { + core_types::FulfillmentStatus::COMPLETE => Some(FulfillmentRequestStatus::Success), + core_types::FulfillmentStatus::CANCELED => Some(FulfillmentRequestStatus::Cancelled), + core_types::FulfillmentStatus::PARTIAL | core_types::FulfillmentStatus::REPLACEMENT => None, + } +} diff --git a/crates/router/src/connector/riskified/transformers/auth.rs b/crates/router/src/connector/riskified/transformers/auth.rs new file mode 100644 index 000000000000..6968bb55a59c --- /dev/null +++ b/crates/router/src/connector/riskified/transformers/auth.rs @@ -0,0 +1,22 @@ +use error_stack; +use masking::{ExposeInterface, Secret}; + +use crate::{core::errors, types}; + +pub struct RiskifiedAuthType { + pub secret_token: Secret, + pub domain_name: String, +} + +impl TryFrom<&types::ConnectorAuthType> for RiskifiedAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + secret_token: api_key.to_owned(), + domain_name: key1.to_owned().expose(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs index 5d9714e4d945..ca6e997ba9ef 100644 --- a/crates/router/src/connector/signifyd.rs +++ b/crates/router/src/connector/signifyd.rs @@ -478,10 +478,10 @@ impl req: &frm_types::FrmFulfillmentRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let req_obj = &req.request.fulfillment_request; + let req_obj = signifyd::FrmFullfillmentSignifydRequest::try_from(req)?; let signifyd_req = types::RequestBody::log_and_get_request_body( &req_obj, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(signifyd_req)) diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index 1a1b09bd2880..66d6f0e48cd5 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -11,10 +11,7 @@ use crate::{ AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, }, - core::{ - errors, - fraud_check::types::{self as core_types, FrmFulfillmentRequest}, - }, + core::{errors, fraud_check::types as core_types}, types::{ self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, ResponseId, ResponseRouterData, @@ -356,7 +353,7 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ #[serde(deny_unknown_fields)] #[serde_with::skip_serializing_none] #[serde(rename_all = "camelCase")] -pub struct FrmFullfillmentSignifydApiRequest { +pub struct FrmFullfillmentSignifydRequest { pub order_id: String, pub fulfillment_status: Option, pub fulfillments: Vec, @@ -391,22 +388,30 @@ pub struct Product { pub item_id: String, } -impl From for FrmFullfillmentSignifydApiRequest { - fn from(req: FrmFulfillmentRequest) -> Self { - Self { - order_id: req.order_id, - fulfillment_status: req.fulfillment_status.map(FulfillmentStatus::from), - fulfillments: req +impl TryFrom<&frm_types::FrmFulfillmentRouterData> for FrmFullfillmentSignifydRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { + Ok(Self { + order_id: item.request.fulfillment_req.order_id.clone(), + fulfillment_status: item + .request + .fulfillment_req + .fulfillment_status + .clone() + .map(|fulfillment_status| FulfillmentStatus::from(&fulfillment_status)), + fulfillments: item + .request + .fulfillment_req .fulfillments .iter() .map(|f| Fulfillments::from(f.clone())) .collect(), - } + }) } } -impl From for FulfillmentStatus { - fn from(status: core_types::FulfillmentStatus) -> Self { +impl From<&core_types::FulfillmentStatus> for FulfillmentStatus { + fn from(status: &core_types::FulfillmentStatus) -> Self { match status { core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 3990fc9c7e47..9283ff41f73a 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -10,6 +10,7 @@ use common_utils::{ errors::ReportSwitchExt, pii::{self, Email, IpAddress}, }; +use data_models::payments::payment_attempt::PaymentAttempt; use diesel_models::enums; use error_stack::{report, IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; @@ -22,13 +23,13 @@ use crate::types::{fraud_check, storage::enums as storage_enums}; use crate::{ consts, core::{ - errors::{self, CustomResult}, + errors::{self, ApiErrorResponse, CustomResult}, payments::PaymentData, }, pii::PeekInterface, types::{ self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, - ApplePayPredecryptData, PaymentsCancelData, ResponseId, + ApplePayPredecryptData, BrowserInformation, PaymentsCancelData, ResponseId, }, utils::{OptionExt, ValueExt}, }; @@ -67,6 +68,8 @@ pub trait RouterData { fn get_return_url(&self) -> Result; fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>; fn get_shipping_address(&self) -> Result<&api::AddressDetails, Error>; + fn get_billing_address_with_phone_number(&self) -> Result<&api::Address, Error>; + fn get_shipping_address_with_phone_number(&self) -> Result<&api::Address, Error>; fn get_connector_meta(&self) -> Result; fn get_session_token(&self) -> Result; fn to_connector_meta(&self) -> Result @@ -176,6 +179,13 @@ impl RouterData for types::RouterData Result<&api::Address, Error> { + self.address + .billing + .as_ref() + .ok_or_else(missing_field_err("billing")) + } fn get_connector_meta(&self) -> Result { self.connector_meta_data .clone() @@ -211,6 +221,14 @@ impl RouterData for types::RouterData Result<&api::Address, Error> { + self.address + .shipping + .as_ref() + .ok_or_else(missing_field_err("shipping")) + } + fn get_payment_method_token(&self) -> Result { self.payment_method_token .clone() @@ -254,7 +272,7 @@ pub trait PaymentsPreProcessingData { fn get_order_details(&self) -> Result, Error>; fn get_webhook_url(&self) -> Result; fn get_return_url(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { @@ -294,7 +312,7 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { .clone() .ok_or_else(missing_field_err("return_url")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -303,14 +321,14 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { pub trait PaymentsCaptureRequestData { fn is_multiple_capture(&self) -> bool; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsCaptureRequestData for types::PaymentsCaptureData { fn is_multiple_capture(&self) -> bool { self.multiple_capture_data.is_some() } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -318,12 +336,12 @@ impl PaymentsCaptureRequestData for types::PaymentsCaptureData { } pub trait PaymentsSetupMandateRequestData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; fn get_email(&self) -> Result; } impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -335,7 +353,7 @@ impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> Result; fn get_email(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; fn get_order_details(&self) -> Result, Error>; fn get_card(&self) -> Result; fn get_return_url(&self) -> Result; @@ -352,11 +370,11 @@ pub trait PaymentsAuthorizeRequestData { } pub trait PaymentMethodTokenizationRequestData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentMethodTokenizationRequestData for types::PaymentMethodTokenizationData { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -374,7 +392,7 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { fn get_email(&self) -> Result { self.email.clone().ok_or_else(missing_field_err("email")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -480,7 +498,7 @@ pub trait BrowserInformationData { fn get_ip_address(&self) -> Result, Error>; } -impl BrowserInformationData for types::BrowserInformation { +impl BrowserInformationData for BrowserInformation { fn get_ip_address(&self) -> Result, Error> { let ip_address = self .ip_address @@ -588,7 +606,7 @@ pub trait PaymentsCancelRequestData { fn get_amount(&self) -> Result; fn get_currency(&self) -> Result; fn get_cancellation_reason(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsCancelRequestData for PaymentsCancelData { @@ -603,7 +621,7 @@ impl PaymentsCancelRequestData for PaymentsCancelData { .clone() .ok_or_else(missing_field_err("cancellation_reason")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -613,7 +631,7 @@ impl PaymentsCancelRequestData for PaymentsCancelData { pub trait RefundsRequestData { fn get_connector_refund_id(&self) -> Result; fn get_webhook_url(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl RefundsRequestData for types::RefundsData { @@ -629,7 +647,7 @@ impl RefundsRequestData for types::RefundsData { .clone() .ok_or_else(missing_field_err("webhook_url")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -1652,3 +1670,83 @@ impl FraudCheckRecordReturnRequest for fraud_check::FraudCheckRecordReturnData { self.currency.ok_or_else(missing_field_err("currency")) } } + +pub trait AccessPaymentAttemptInfo { + fn get_browser_info( + &self, + ) -> Result, error_stack::Report>; +} + +impl AccessPaymentAttemptInfo for PaymentAttempt { + fn get_browser_info( + &self, + ) -> Result, error_stack::Report> { + self.browser_info + .clone() + .map(|b| b.parse_value("BrowserInformation")) + .transpose() + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + }) + } +} + +pub trait PaymentsAttemptData { + fn get_browser_info(&self) + -> Result>; +} + +impl PaymentsAttemptData for PaymentAttempt { + fn get_browser_info( + &self, + ) -> Result> { + self.browser_info + .clone() + .ok_or(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + })? + .parse_value::("BrowserInformation") + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + }) + } +} + +#[cfg(feature = "frm")] +pub trait FrmTransactionRouterDataRequest { + fn is_payment_successful(&self) -> Option; +} + +#[cfg(feature = "frm")] +impl FrmTransactionRouterDataRequest for fraud_check::FrmTransactionRouterData { + fn is_payment_successful(&self) -> Option { + match self.status { + storage_enums::AttemptStatus::AuthenticationFailed + | storage_enums::AttemptStatus::RouterDeclined + | storage_enums::AttemptStatus::AuthorizationFailed + | storage_enums::AttemptStatus::Voided + | storage_enums::AttemptStatus::CaptureFailed + | storage_enums::AttemptStatus::Failure + | storage_enums::AttemptStatus::AutoRefunded => Some(false), + + storage_enums::AttemptStatus::AuthenticationSuccessful + | storage_enums::AttemptStatus::PartialChargedAndChargeable + | storage_enums::AttemptStatus::Authorized + | storage_enums::AttemptStatus::Charged => Some(true), + + storage_enums::AttemptStatus::Started + | storage_enums::AttemptStatus::AuthenticationPending + | storage_enums::AttemptStatus::Authorizing + | storage_enums::AttemptStatus::CodInitiated + | storage_enums::AttemptStatus::VoidInitiated + | storage_enums::AttemptStatus::CaptureInitiated + | storage_enums::AttemptStatus::VoidFailed + | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::Unresolved + | storage_enums::AttemptStatus::Pending + | storage_enums::AttemptStatus::PaymentMethodAwaited + | storage_enums::AttemptStatus::ConfirmationAwaited + | storage_enums::AttemptStatus::DeviceDataCollectionPending => None, + } + } +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index f5bb357af0ba..f6aaf22480b9 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -13,7 +13,6 @@ use common_utils::{ use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; -use pm_auth::connector::plaid::transformers::PlaidAuthType; use uuid::Uuid; use crate::{ @@ -1859,10 +1858,12 @@ pub(crate) fn validate_auth_and_metadata_type( signifyd::transformers::SignifydAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Plaid => { - PlaidAuthType::foreign_try_from(val)?; + api_enums::Connector::Riskified => { + riskified::transformers::RiskifiedAuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Plaid => Err(report!(errors::ConnectorError::InvalidConnectorName) + .attach_printable(format!("invalid connector name: {connector_name}"))), } } diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 55bd22baeec4..850c6c8322f8 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -17,7 +17,6 @@ use self::{ }; use super::errors::{ConnectorErrorExt, RouterResponse}; use crate::{ - connector::signifyd::transformers::FrmFullfillmentSignifydApiRequest, core::{ errors::{self, RouterResult}, payments::{ @@ -52,7 +51,7 @@ pub mod types; pub async fn call_frm_service( state: &AppState, payment_data: &mut payments::PaymentData, - frm_data: FrmData, + frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, @@ -78,7 +77,12 @@ where ) .await?; - let router_data = frm_data + frm_data.payment_attempt.connector_transaction_id = payment_data + .payment_attempt + .connector_transaction_id + .clone(); + + let mut router_data = frm_data .construct_router_data( state, &frm_data.connector_details.connector_name, @@ -88,6 +92,9 @@ where &merchant_connector_account, ) .await?; + + router_data.status = payment_data.payment_attempt.status; + let connector = FraudCheckConnectorData::get_connector_by_name(&frm_data.connector_details.connector_name)?; let router_data_res = router_data @@ -397,6 +404,7 @@ where address: payment_data.address.clone(), connector_details: frm_connector_details.clone(), order_details, + frm_metadata: payment_data.frm_metadata.clone(), }; let fraud_check_operation: operation::BoxedFraudCheckOperation = @@ -722,15 +730,14 @@ pub async fn make_fulfillment_api_call( frm_types::FraudCheckFulfillmentData, frm_types::FraudCheckResponseData, > = connector_data.connector.get_connector_integration(); - let modified_request_for_api_call = FrmFullfillmentSignifydApiRequest::from(req); let router_data = frm_flows::fulfillment_flow::construct_fulfillment_router_data( &state, &payment_intent, &payment_attempt, &merchant_account, &key_store, - "signifyd".to_string(), - modified_request_for_api_call, + fraud_check.frm_name.clone(), + req, ) .await?; let response = services::execute_connector_processing_step( diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 47a29d657484..7f8993af5270 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -1,9 +1,11 @@ use async_trait::async_trait; -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii::Email}; use error_stack::ResultExt; +use masking::ExposeInterface; use super::{ConstructFlowSpecificData, FeatureFrm}; use crate::{ + connector::utils::PaymentsAttemptData, core::{ errors::{ConnectorErrorExt, RouterResult}, fraud_check::types::FrmData, @@ -15,7 +17,7 @@ use crate::{ domain, fraud_check::{FraudCheckCheckoutData, FraudCheckResponseData, FrmCheckoutRouterData}, storage::enums as storage_enums, - ConnectorAuthType, ResponseId, RouterData, + BrowserInformation, ConnectorAuthType, ResponseId, RouterData, }, AppState, }; @@ -43,6 +45,7 @@ impl ConstructFlowSpecificData = self.payment_attempt.get_browser_info().ok(); let customer_id = customer.to_owned().map(|customer| customer.customer_id); let router_data = RouterData { @@ -68,6 +71,27 @@ impl ConstructFlowSpecificData( + "AdditionalPaymentData", + ) + }) + .transpose() + .unwrap_or_default(), + email: customer.clone().and_then(|customer_data| { + customer_data + .email + .and_then(|email| Email::try_from(email.into_inner().expose()).ok()) + }), + gateway: self.payment_attempt.connector.clone(), }, // self.order_details response: Ok(FraudCheckResponseData::TransactionResponse { resource_id: ResponseId::ConnectorTransactionId("".to_string()), @@ -94,6 +118,7 @@ impl ConstructFlowSpecificData( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, connector: String, - fulfillment_request: FrmFullfillmentSignifydApiRequest, + fulfillment_request: FrmFulfillmentRequest, ) -> RouterResult { let profile_id = core_utils::get_profile_id_from_business_details( payment_intent.business_country, @@ -79,7 +79,7 @@ pub async fn construct_fulfillment_router_data<'a>( request: FraudCheckFulfillmentData { amount: payment_attempt.amount, order_details: payment_intent.order_details.clone(), - fulfillment_request, + fulfillment_req: fulfillment_request, }, response: Err(ErrorResponse::default()), access_token: None, @@ -105,6 +105,7 @@ pub async fn construct_fulfillment_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index eaefdbefcc77..bd0ba3e4f7f4 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -96,6 +96,7 @@ impl ConstructFlowSpecificData for FraudCheckPost { connector_details: payment_data.connector_details, order_details: payment_data.order_details, refund: None, + frm_metadata: payment_data.frm_metadata, }; Ok(Some(frm_data)) } @@ -152,7 +153,7 @@ impl Domain for FraudCheckPost { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -219,7 +220,7 @@ impl Domain for FraudCheckPost { let _router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -243,7 +244,7 @@ impl Domain for FraudCheckPost { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs index 00f50d01a862..b92df3d3ef9f 100644 --- a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -120,6 +120,7 @@ impl GetTracker for FraudCheckPre { connector_details: payment_data.connector_details, order_details: payment_data.order_details, refund: None, + frm_metadata: payment_data.frm_metadata, }; Ok(Some(frm_data)) } @@ -146,7 +147,7 @@ impl Domain for FraudCheckPre { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -163,6 +164,9 @@ impl Domain for FraudCheckPre { order_details: router_data.request.order_details, currency: router_data.request.currency, payment_method: Some(router_data.payment_method), + error_code: router_data.request.error_code, + error_message: router_data.request.error_message, + connector_transaction_id: router_data.request.connector_transaction_id, }), response: FrmResponse::Transaction(router_data.response), })) @@ -180,7 +184,7 @@ impl Domain for FraudCheckPre { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -195,6 +199,11 @@ impl Domain for FraudCheckPre { request: FrmRequest::Checkout(FraudCheckCheckoutData { amount: router_data.request.amount, order_details: router_data.request.order_details, + currency: router_data.request.currency, + browser_info: router_data.request.browser_info, + payment_method_data: router_data.request.payment_method_data, + email: router_data.request.email, + gateway: router_data.request.gateway, }), response: FrmResponse::Checkout(router_data.response), }) diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs index 1d6e7cb45a58..e60458646f3e 100644 --- a/crates/router/src/core/fraud_check/types.rs +++ b/crates/router/src/core/fraud_check/types.rs @@ -56,6 +56,7 @@ pub struct FrmData { pub connector_details: ConnectorDetailsCore, pub order_details: Option>, pub refund: Option, + pub frm_metadata: Option, } #[derive(Debug)] @@ -79,6 +80,7 @@ pub struct PaymentToFrmData { pub address: PaymentAddress, pub connector_details: ConnectorDetailsCore, pub order_details: Option>, + pub frm_metadata: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -123,6 +125,14 @@ pub struct FrmFulfillmentRequest { ///contains details of the fulfillment #[schema(value_type = Vec)] pub fulfillments: Vec, + //name of the tracking Company + #[schema(max_length = 255, example = "fedex")] + pub tracking_company: Option, + //tracking ID of the product + #[schema(max_length = 255, example = "track_8327446667")] + pub tracking_number: Option, + //tracking_url for tracking the product + pub tracking_url: Option, } #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 73af17f9d66b..3c7ac0fd78d0 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1941,6 +1941,7 @@ where pub payment_link_data: Option, pub incremental_authorization_details: Option, pub authorizations: Vec, + pub frm_metadata: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 0cb91de05992..394051f1432b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -173,6 +173,7 @@ default_imp_for_complete_authorize!( connector::Payu, connector::Placetopay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -251,6 +252,7 @@ default_imp_for_webhook_source_verification!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -332,6 +334,7 @@ default_imp_for_create_customer!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -402,6 +405,7 @@ default_imp_for_connector_redirect_response!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -463,6 +467,7 @@ default_imp_for_connector_request_id!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -547,6 +552,7 @@ default_imp_for_accept_dispute!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -649,6 +655,7 @@ default_imp_for_file_upload!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -729,6 +736,7 @@ default_imp_for_submit_evidence!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -809,6 +817,7 @@ default_imp_for_defend_dispute!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -888,6 +897,7 @@ default_imp_for_pre_processing_steps!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -951,6 +961,7 @@ default_imp_for_payouts!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1032,6 +1043,7 @@ default_imp_for_payouts_create!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1116,6 +1128,7 @@ default_imp_for_payouts_eligibility!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1197,6 +1210,7 @@ default_imp_for_payouts_fulfill!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1278,6 +1292,7 @@ default_imp_for_payouts_cancel!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1360,6 +1375,7 @@ default_imp_for_payouts_quote!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1442,6 +1458,7 @@ default_imp_for_payouts_recipient!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1523,6 +1540,7 @@ default_imp_for_approve!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1605,6 +1623,7 @@ default_imp_for_reject!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -2166,6 +2185,7 @@ default_imp_for_incremental_authorization!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index e05c60dcf341..47d9c0820d45 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2999,6 +2999,7 @@ pub fn router_data_type_conversion( connector_http_status_code: router_data.connector_http_status_code, external_latency: router_data.external_latency, apple_pay_flow: router_data.apple_pay_flow, + frm_metadata: router_data.frm_metadata, } } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 37a3e1a14123..7d0ec0718c25 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -257,6 +257,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 7c8fbcc34979..7e6572ff07dc 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -173,6 +173,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 65b91f0401cf..19998a9a0a71 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -217,6 +217,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 48b503b96b0d..3e91a09ab54e 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -253,6 +253,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: None, }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 612ddadc1c59..8481cd5c8360 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -488,6 +488,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index cbce6ba9e970..87a6b6927513 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -333,6 +333,7 @@ impl payment_link_data, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 693fce236846..9ea347afd735 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -186,6 +186,7 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + frm_metadata: None, }, Some(payments::CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 03bf6dd46b60..5cb3c95dc257 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -160,6 +160,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 572bc710b963..7d9c37339349 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -197,6 +197,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 887edd030d13..67c8579d263a 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -171,6 +171,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 0320cf50663e..44fbdf107818 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -423,6 +423,7 @@ async fn get_tracker_for_sync< frm_message: frm_response.ok(), incremental_authorization_details: None, authorizations, + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 84f11124c730..3ae0aae6d111 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -375,6 +375,7 @@ impl payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], + frm_metadata: request.frm_metadata.clone(), }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 7346c46df120..3a0dfd19a650 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -149,6 +149,7 @@ impl authorization_id: None, }), authorizations: vec![], + frm_metadata: None, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5c280ed72d3b..2aaf0b2957f6 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -165,6 +165,7 @@ where connector_http_status_code: None, external_latency: None, apple_pay_flow, + frm_metadata: None, }; Ok(router_data) @@ -945,6 +946,11 @@ pub fn change_order_details_to_new_type( quantity: order_details.quantity, amount: order_amount, product_img_link: order_details.product_img_link, + requires_shipping: order_details.requires_shipping, + product_id: order_details.product_id, + category: order_details.category, + brand: order_details.brand, + product_type: order_details.product_type, }]) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 50d9be82794b..016d5ec955d2 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -175,6 +175,7 @@ pub async fn construct_payout_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) @@ -326,6 +327,7 @@ pub async fn construct_refund_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) @@ -555,6 +557,7 @@ pub async fn construct_accept_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } @@ -642,6 +645,7 @@ pub async fn construct_submit_evidence_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } @@ -735,6 +739,7 @@ pub async fn construct_upload_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } @@ -825,6 +830,7 @@ pub async fn construct_defend_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } @@ -908,6 +914,7 @@ pub async fn construct_retrieve_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index 322440e53138..08b490480434 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -113,6 +113,7 @@ pub async fn construct_webhook_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 95c36719cad1..c7d3b11abbc8 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -268,6 +268,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::SecretInfoToInitiateSdk, api_models::payments::ApplePayPaymentRequest, api_models::payments::AmountInfo, + api_models::payments::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index aa563c647eaa..f50a936aac8b 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -308,6 +308,8 @@ pub struct RouterData { pub external_latency: Option, /// Contains apple pay flow type simplified or manual pub apple_pay_flow: Option, + + pub frm_metadata: Option, } #[derive(Debug, Clone, serde::Deserialize)] @@ -1282,6 +1284,7 @@ impl From<(&RouterData, T2)> connector_http_status_code: data.connector_http_status_code, external_latency: data.external_latency, apple_pay_flow: data.apple_pay_flow.clone(), + frm_metadata: data.frm_metadata.clone(), } } } @@ -1337,6 +1340,7 @@ impl connector_http_status_code: data.connector_http_status_code, external_latency: data.external_latency, apple_pay_flow: None, + frm_metadata: None, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 71751adf6ba4..fcbd3801c944 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -406,7 +406,9 @@ impl ConnectorData { enums::Connector::Tsys => Ok(Box::new(&connector::Tsys)), enums::Connector::Volt => Ok(Box::new(&connector::Volt)), enums::Connector::Zen => Ok(Box::new(&connector::Zen)), - enums::Connector::Signifyd | enums::Connector::Plaid => { + enums::Connector::Signifyd + | enums::Connector::Plaid + | enums::Connector::Riskified => { Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs index 7be60bfee952..e871cbc5d330 100644 --- a/crates/router/src/types/api/fraud_check.rs +++ b/crates/router/src/types/api/fraud_check.rs @@ -86,6 +86,7 @@ impl FraudCheckConnectorData { ) -> CustomResult { match connector_name { enums::FrmConnectors::Signifyd => Ok(Box::new(&connector::Signifyd)), + enums::FrmConnectors::Riskified => Ok(Box::new(&connector::Riskified)), } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 74b15f911b9a..04a9d76632b0 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -100,6 +100,7 @@ impl VerifyConnectorData { connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, } } } diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs index 4bbba8ac4dca..191e74422f91 100644 --- a/crates/router/src/types/fraud_check.rs +++ b/crates/router/src/types/fraud_check.rs @@ -1,9 +1,13 @@ +use common_utils::pii::Email; + use crate::{ - connector::signifyd::transformers::{FrmFullfillmentSignifydApiRequest, RefundMethod}, + connector::signifyd::transformers::RefundMethod, + core::fraud_check::types::FrmFulfillmentRequest, pii::Serialize, services, - types::{api, storage_enums, ErrorResponse, ResponseId, RouterData}, + types::{self, api, storage_enums, ErrorResponse, ResponseId, RouterData}, }; + pub type FrmSaleRouterData = RouterData; pub type FrmSaleType = @@ -74,6 +78,11 @@ pub type FrmCheckoutType = dyn services::ConnectorIntegration< pub struct FraudCheckCheckoutData { pub amount: i64, pub order_details: Option>, + pub currency: Option, + pub browser_info: Option, + pub payment_method_data: Option, + pub email: Option, + pub gateway: Option, } pub type FrmTransactionRouterData = @@ -91,6 +100,9 @@ pub struct FraudCheckTransactionData { pub order_details: Option>, pub currency: Option, pub payment_method: Option, + pub error_code: Option, + pub error_message: Option, + pub connector_transaction_id: Option, } pub type FrmFulfillmentRouterData = @@ -114,7 +126,7 @@ pub type FrmRecordReturnType = dyn services::ConnectorIntegration< pub struct FraudCheckFulfillmentData { pub amount: i64, pub order_details: Option>>, - pub fulfillment_request: FrmFullfillmentSignifydApiRequest, + pub fulfillment_req: FrmFulfillmentRequest, } #[derive(Debug, Clone)] diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 9f5d46aba426..5bd03fe833af 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -229,6 +229,12 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { }) .into_report()? } + api_enums::Connector::Riskified => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "riskified is not a routable connector".to_string(), + }) + .into_report()? + } api_enums::Connector::Square => Self::Square, api_enums::Connector::Stax => Self::Stax, api_enums::Connector::Stripe => Self::Stripe, diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index dd8c1ed5f778..c46cca8e4dd7 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -95,6 +95,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, } } @@ -153,6 +154,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, } } diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 7de81a8bed28..206694be0e8b 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -78,6 +78,11 @@ fn payment_method_details() -> Option { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -373,6 +378,11 @@ async fn should_fail_payment_for_incorrect_cvc() { quantity: 1, amount: 100, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -406,6 +416,11 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 100, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -439,6 +454,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() { quantity: 1, amount: 100, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index d3b20b01e4ce..1c384b2e5137 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -523,6 +523,7 @@ pub trait ConnectorActions: Connector { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, } } diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index 12f914e13c1a..c3bce7d51c39 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -315,6 +315,11 @@ async fn should_fail_payment_for_incorrect_card_number() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -351,6 +356,11 @@ async fn should_fail_payment_for_incorrect_cvc() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -387,6 +397,11 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -423,6 +438,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() { quantity: 1, amount: 1000, product_img_link: None, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index cfe8ed561466..ae82a0c094dd 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -43,4 +43,4 @@ actix_web = ["tracing-actix-web"] log_custom_entries_to_extra = [] log_extra_implicit_fields = [] log_active_span_json = [] -payouts = [] +payouts = [] \ No newline at end of file diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index c8c6b1921183..883c243c6c44 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -115,6 +115,7 @@ placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 3c0206a15896..3ffb98e56b95 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4396,7 +4396,8 @@ "worldpay", "zen", "signifyd", - "plaid" + "plaid", + "riskified" ] }, "ConnectorMetadata": { @@ -8116,10 +8117,37 @@ "example": 1, "minimum": 0 }, + "requires_shipping": { + "type": "boolean", + "nullable": true + }, "product_img_link": { "type": "string", "description": "The image URL of the product", "nullable": true + }, + "product_id": { + "type": "string", + "description": "ID of the product that is being purchased", + "nullable": true + }, + "category": { + "type": "string", + "description": "Category of the product that is being purchased", + "nullable": true + }, + "brand": { + "type": "string", + "description": "Brand of the product that is being purchased", + "nullable": true + }, + "product_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ProductType" + } + ], + "nullable": true } } }, @@ -8149,10 +8177,37 @@ "format": "int64", "description": "the amount per quantity of product" }, + "requires_shipping": { + "type": "boolean", + "nullable": true + }, "product_img_link": { "type": "string", "description": "The image URL of the product", "nullable": true + }, + "product_id": { + "type": "string", + "description": "ID of the product that is being purchased", + "nullable": true + }, + "category": { + "type": "string", + "description": "Category of the product that is being purchased", + "nullable": true + }, + "brand": { + "type": "string", + "description": "Brand of the product that is being purchased", + "nullable": true + }, + "product_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ProductType" + } + ], + "nullable": true } } }, @@ -9788,6 +9843,10 @@ "type": "boolean", "description": "Request for an incremental authorization", "nullable": true + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, @@ -10158,6 +10217,10 @@ "type": "boolean", "description": "Request for an incremental authorization", "nullable": true + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, @@ -11268,6 +11331,17 @@ } } }, + "ProductType": { + "type": "string", + "enum": [ + "physical", + "digital", + "travel", + "ride", + "event", + "accommodation" + ] + }, "ReceiverDetails": { "type": "object", "required": [ From e1e23fd987cae96e56311d1cfdcb225d9327860c Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Tue, 12 Dec 2023 20:33:45 +0530 Subject: [PATCH 186/443] fix(docker-compose): remove label list from docker compose yml (#3118) --- docker-compose-development.yml | 4 ---- docker-compose.yml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/docker-compose-development.yml b/docker-compose-development.yml index 500f397cfa30..5a3eca4cdf35 100644 --- a/docker-compose-development.yml +++ b/docker-compose-development.yml @@ -28,8 +28,6 @@ services: redis-standalone: image: redis:7 - labels: - - redis networks: - router_net ports: @@ -159,8 +157,6 @@ services: - clustered_redis volumes: - ./config/redis.conf:/usr/local/etc/redis/redis.conf - labels: - - redis networks: - router_net ports: diff --git a/docker-compose.yml b/docker-compose.yml index f51a47aee940..9f8e7bb4efba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,8 +24,6 @@ services: redis-standalone: image: redis:7 - labels: - - redis networks: - router_net ports: @@ -133,8 +131,6 @@ services: - clustered_redis volumes: - ./config/redis.conf:/usr/local/etc/redis/redis.conf - labels: - - redis networks: - router_net ports: From 9d010b14df24a5e60cb743f0b6cb8257745fe005 Mon Sep 17 00:00:00 2001 From: Shanks Date: Tue, 12 Dec 2023 20:34:37 +0530 Subject: [PATCH 187/443] ci: add missing default and release routing features and add makefile command to build wasm (#3107) --- Makefile | 14 +++++++++++++- crates/euclid_wasm/Cargo.toml | 1 + crates/router/Cargo.toml | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9b62b3c5c99d..780d5a993c92 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,10 @@ eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\ $(findstring $(2),$(1))),1) + +ROOT_DIR_WITH_SLASH := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) +ROOT_DIR := $(realpath $(ROOT_DIR_WITH_SLASH)) + # # = Targets # @@ -67,6 +71,14 @@ fmt : clippy : cargo clippy --all-features --all-targets -- -D warnings +# Build the DSL crate as a WebAssembly JS library +# +# Usage : +# make euclid-wasm + +euclid-wasm: + wasm-pack build --target web --out-dir $(ROOT_DIR)/wasm --out-name euclid $(ROOT_DIR)/crates/euclid_wasm -- --features dummy_connector + # Run Rust tests of project. # # Usage : @@ -93,4 +105,4 @@ precommit : fmt clippy test hack: - cargo hack check --workspace --each-feature --all-targets \ No newline at end of file + cargo hack check --workspace --each-feature --all-targets diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 8c96a7f67da2..51288a9d0feb 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [features] default = ["connector_choice_bcompat"] +release = ["connector_choice_bcompat", "connector_choice_mca_id"] connector_choice_bcompat = ["api_models/connector_choice_bcompat"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] dummy_connector = ["kgraph_utils/dummy_connector"] diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b2f7f8b94a97..13324aa59a29 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,14 +9,14 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry", "frm"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] +release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] From 1add2c059f4fb5653f33e2f3ce454793caf2d595 Mon Sep 17 00:00:00 2001 From: Sasha Zoria Date: Tue, 12 Dec 2023 20:49:47 +0200 Subject: [PATCH 188/443] refactor(connector): [Wise] Error Message For Connector Implementation (#2952) Co-authored-by: Oleksandr.Zoria --- .../router/src/connector/wise/transformers.rs | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/crates/router/src/connector/wise/transformers.rs b/crates/router/src/connector/wise/transformers.rs index c481f0c73433..e0fc05c3c89e 100644 --- a/crates/router/src/connector/wise/transformers.rs +++ b/crates/router/src/connector/wise/transformers.rs @@ -11,7 +11,7 @@ type Error = error_stack::Report; #[cfg(feature = "payouts")] use crate::{ - connector::utils::RouterData, + connector::utils::{self, RouterData}, types::{ api::payouts, storage::enums::{self as storage_enums, PayoutEntityType}, @@ -344,10 +344,9 @@ fn get_payout_bank_details( bic: b.bic, ..WiseBankDetails::default() }), - _ => Err(errors::ConnectorError::NotSupported { - message: "Card payout creation is not supported".to_string(), - connector: "Wise", - }), + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } @@ -371,10 +370,9 @@ impl TryFrom<&types::PayoutsRouterData> for WiseRecipientCreateRequest { }), }?; match request.payout_type.to_owned() { - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout creation is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, storage_enums::PayoutType::Bank => { let account_holder_name = customer_details .ok_or(errors::ConnectorError::MissingRequiredField { @@ -432,10 +430,9 @@ impl TryFrom<&types::PayoutsRouterData> for WisePayoutQuoteRequest { target_currency: request.destination_currency.to_string(), pay_out: WisePayOutOption::default(), }), - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout fulfillment is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } } @@ -489,10 +486,9 @@ impl TryFrom<&types::PayoutsRouterData> for WisePayoutCreateRequest { details: wise_transfer_details, }) } - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout fulfillment is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } } @@ -533,10 +529,9 @@ impl TryFrom<&types::PayoutsRouterData> for WisePayoutFulfillRequest { storage_enums::PayoutType::Bank => Ok(Self { fund_type: FundType::default(), }), - storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotSupported { - message: "Card payout fulfillment is not supported".to_string(), - connector: "Wise", - })?, + storage_enums::PayoutType::Card => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ))?, } } } @@ -599,10 +594,9 @@ impl TryFrom for RecipientType { PayoutMethodData::Bank(api_models::payouts::Bank::Ach(_)) => Ok(Self::Aba), PayoutMethodData::Bank(api_models::payouts::Bank::Bacs(_)) => Ok(Self::SortCode), PayoutMethodData::Bank(api_models::payouts::Bank::Sepa(_)) => Ok(Self::Iban), - _ => Err(errors::ConnectorError::NotSupported { - message: "Requested payout_method_type is not supported".to_string(), - connector: "Wise", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Wise"), + ) .into()), } } From 3f4167dbd477c793e1a4cc572da0c12d66f2b649 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Wed, 13 Dec 2023 10:17:15 +0530 Subject: [PATCH 189/443] fix(api_locking): fix the unit interpretation for `LockSettings` expiry (#3121) --- crates/router/src/configs/settings.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f73fe099c295..b62831950856 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -897,11 +897,11 @@ impl<'de> Deserialize<'de> for LockSettings { redis_lock_expiry_seconds, delay_between_retries_in_milliseconds, } = Inner::deserialize(deserializer)?; - let redis_lock_expiry_seconds = redis_lock_expiry_seconds * 1000; + let redis_lock_expiry_milliseconds = redis_lock_expiry_seconds * 1000; Ok(Self { redis_lock_expiry_seconds, delay_between_retries_in_milliseconds, - lock_retries: redis_lock_expiry_seconds / delay_between_retries_in_milliseconds, + lock_retries: redis_lock_expiry_milliseconds / delay_between_retries_in_milliseconds, }) } } From 6e82b0bd746b405281f79b86a3cd92b550a33f68 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Wed, 13 Dec 2023 11:26:03 +0530 Subject: [PATCH 190/443] feat(events): add type info to outgoing requests & maintain structural & PII type info (#2956) Co-authored-by: Prasunna Soppa --- Cargo.lock | 16 +- config/config.example.toml | 27 ++ config/development.toml | 2 +- config/docker_compose.toml | 2 +- connector-template/mod.rs | 15 +- crates/common_utils/src/request.rs | 77 +++--- crates/masking/Cargo.toml | 1 + crates/masking/src/serde.rs | 25 +- crates/pm_auth/src/connector/plaid.rs | 39 +-- crates/pm_auth/src/types/api.rs | 6 +- crates/router/Cargo.toml | 1 + crates/router/src/connector/aci.rs | 38 +-- crates/router/src/connector/adyen.rs | 236 +++++++----------- crates/router/src/connector/airwallex.rs | 81 ++---- .../router/src/connector/authorizedotnet.rs | 81 ++---- crates/router/src/connector/bambora.rs | 66 ++--- crates/router/src/connector/bankofamerica.rs | 64 ++--- crates/router/src/connector/bitpay.rs | 17 +- crates/router/src/connector/bluesnap.rs | 76 ++---- crates/router/src/connector/boku.rs | 63 ++--- crates/router/src/connector/braintree.rs | 150 ++++------- crates/router/src/connector/cashtocode.rs | 22 +- crates/router/src/connector/checkout.rs | 91 +++---- crates/router/src/connector/coinbase.rs | 17 +- crates/router/src/connector/cryptopay.rs | 44 ++-- crates/router/src/connector/cybersource.rs | 95 ++----- crates/router/src/connector/dlocal.rs | 49 ++-- crates/router/src/connector/dummyconnector.rs | 33 +-- crates/router/src/connector/fiserv.rs | 85 ++----- crates/router/src/connector/forte.rs | 47 ++-- crates/router/src/connector/globalpay.rs | 81 ++---- crates/router/src/connector/globepay.rs | 31 +-- crates/router/src/connector/gocardless.rs | 70 ++---- crates/router/src/connector/helcim.rs | 64 ++--- crates/router/src/connector/iatapay.rs | 45 ++-- crates/router/src/connector/klarna.rs | 25 +- crates/router/src/connector/mollie.rs | 40 +-- crates/router/src/connector/multisafepay.rs | 32 +-- crates/router/src/connector/nexinets.rs | 51 ++-- crates/router/src/connector/nmi.rs | 83 ++---- crates/router/src/connector/noon.rs | 56 ++--- crates/router/src/connector/nuvei.rs | 107 +++----- crates/router/src/connector/opayo.rs | 35 +-- crates/router/src/connector/opennode.rs | 17 +- crates/router/src/connector/payeezy.rs | 59 ++--- crates/router/src/connector/payme.rs | 108 +++----- crates/router/src/connector/paypal.rs | 69 ++--- crates/router/src/connector/payu.rs | 49 ++-- crates/router/src/connector/placetopay.rs | 31 +-- crates/router/src/connector/powertranz.rs | 67 ++--- crates/router/src/connector/prophetpay.rs | 80 ++---- crates/router/src/connector/rapyd.rs | 60 ++--- crates/router/src/connector/riskified.rs | 48 ++-- crates/router/src/connector/shift4.rs | 57 ++--- crates/router/src/connector/signifyd.rs | 59 ++--- crates/router/src/connector/square.rs | 43 ++-- crates/router/src/connector/stax.rs | 67 ++--- crates/router/src/connector/stripe.rs | 138 ++++------ .../src/connector/stripe/transformers.rs | 19 +- crates/router/src/connector/trustpay.rs | 71 ++---- crates/router/src/connector/tsys.rs | 79 ++---- crates/router/src/connector/volt.rs | 46 ++-- crates/router/src/connector/wise.rs | 48 ++-- crates/router/src/connector/worldline.rs | 43 ++-- crates/router/src/connector/worldpay.rs | 34 +-- crates/router/src/connector/zen.rs | 32 +-- .../src/core/payment_methods/transformers.rs | 52 ++-- .../src/core/payments/flows/session_flow.rs | 13 +- crates/router/src/core/refunds.rs | 4 +- crates/router/src/core/verification.rs | 18 +- crates/router/src/core/webhooks.rs | 24 +- crates/router/src/core/webhooks/types.rs | 2 +- crates/router/src/events.rs | 2 + .../router/src/events/connector_api_logs.rs | 69 +++++ crates/router/src/routes/admin.rs | 4 +- crates/router/src/routes/app.rs | 5 +- crates/router/src/routes/locker_migration.rs | 4 +- crates/router/src/services/api.rs | 135 +++++++--- crates/router/src/services/kafka.rs | 14 +- crates/router/src/types.rs | 2 +- .../src/utils/connector_onboarding/paypal.rs | 17 +- .../Payments - Create/event.test.js | 4 +- .../Payments - Create/request.json | 2 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Confirm/event.test.js | 4 +- .../Payments - Create/request.json | 2 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Confirm/event.test.js | 4 +- .../Payments - Retrieve/event.test.js | 4 +- .../Payments - Capture/event.test.js | 6 +- .../Payments - Create/request.json | 2 +- .../Payments - Retrieve/event.test.js | 6 +- .../bankofamerica.postman_collection.json | 34 +-- 93 files changed, 1496 insertions(+), 2555 deletions(-) create mode 100644 crates/router/src/events/connector_api_logs.rs diff --git a/Cargo.lock b/Cargo.lock index 5f1ab2cd3423..b58631632e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,7 +1668,7 @@ dependencies = [ "once_cell", "phonenumber", "proptest", - "quick-xml", + "quick-xml 0.28.2", "rand 0.8.5", "regex", "reqwest", @@ -3539,6 +3539,7 @@ version = "0.1.0" dependencies = [ "bytes 1.5.0", "diesel", + "erased-serde", "serde", "serde_json", "subtle", @@ -4330,7 +4331,7 @@ dependencies = [ "itertools 0.11.0", "lazy_static", "nom", - "quick-xml", + "quick-xml 0.28.2", "regex", "regex-cache", "serde", @@ -4621,6 +4622,16 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.33" @@ -5087,6 +5098,7 @@ dependencies = [ "openssl", "pm_auth", "qrcode", + "quick-xml 0.31.0", "rand 0.8.5", "rand_chacha 0.3.1", "rdkafka", diff --git a/config/config.example.toml b/config/config.example.toml index 05fdb8827632..8618a4031f9f 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -476,6 +476,12 @@ pm_auth_key = "Some_pm_auth_key" [analytics] source = "sqlx" # The Analytics source/strategy to be used +[analytics.clickhouse] +username = "" # Clickhouse username +password = "" # Clickhouse password (optional) +host = "" # Clickhouse host in http(s)://: format +database_name = "" # Clickhouse database name + [analytics.sqlx] username = "db_user" # Analytics DB Username password = "db_pass" # Analytics DB Password @@ -484,6 +490,7 @@ port = 5432 # Analytics DB Port dbname = "hyperswitch_db" # Name of Database pool_size = 5 # Number of connections to keep open connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client # Config for KV setup [kv_config] @@ -498,3 +505,23 @@ client_id = "paypal_client_id" # Client ID for PayPal onboarding client_secret = "paypal_secret_key" # Secret key for PayPal onboarding partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding enabled = true # Switch to enable or disable PayPal onboarding + +[frm] +enabled = true + +[paypal_onboarding] +client_id = "paypal_client_id" # Client ID for PayPal onboarding +client_secret = "paypal_secret_key" # Secret key for PayPal onboarding +partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding +enabled = true # Switch to enable or disable PayPal onboarding + +[events] +source = "logs" # The event sink to push events supports kafka or logs (stdout) + +[events.kafka] +brokers = [] # Kafka broker urls for bootstrapping the client +intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events +attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events +refund_analytics_topic = "topic" # Kafka topic to be used for Refund events +api_logs_topic = "topic" # Kafka topic to be used for incoming api events +connector_logs_topic = "topic" # Kafka topic to be used for connector api events \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index 0b7b9756b477..161914361039 100644 --- a/config/development.toml +++ b/config/development.toml @@ -513,7 +513,7 @@ intent_analytics_topic = "hyperswitch-payment-intent-events" attempt_analytics_topic = "hyperswitch-payment-attempt-events" refund_analytics_topic = "hyperswitch-refund-events" api_logs_topic = "hyperswitch-api-log-events" -connector_events_topic = "hyperswitch-connector-api-events" +connector_logs_topic = "hyperswitch-connector-api-events" [analytics] source = "sqlx" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 35c97e6b5967..c89bec1999dc 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -360,7 +360,7 @@ intent_analytics_topic = "hyperswitch-payment-intent-events" attempt_analytics_topic = "hyperswitch-payment-attempt-events" refund_analytics_topic = "hyperswitch-refund-events" api_logs_topic = "hyperswitch-api-log-events" -connector_events_topic = "hyperswitch-connector-api-events" +connector_logs_topic = "hyperswitch-connector-api-events" [analytics] source = "sqlx" diff --git a/connector-template/mod.rs b/connector-template/mod.rs index e9945a726a95..6258d4370768 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -15,6 +15,7 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, + RequestContent } }; @@ -158,7 +159,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors,) -> CustomResult { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -187,7 +188,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?) + .set_body(types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?) .build(), )) } @@ -304,7 +305,7 @@ impl &self, _req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -321,7 +322,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body(self, req, connectors)?) + .set_body(types::PaymentsCaptureType::get_request_body(self, req, connectors)?) .build(), )) } @@ -376,7 +377,7 @@ impl Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) } - fn get_request_body(&self, req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult, errors::ConnectorError> { + fn get_request_body(&self, req: &types::RefundsRouterData, _connectors: &settings::Connectors,) -> CustomResult { let connector_router_data = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RouterData::try_from(( &self.get_currency_unit(), @@ -396,7 +397,7 @@ impl .url(&types::RefundExecuteType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundExecuteType::get_headers(self, req, connectors)?) - .body(types::RefundExecuteType::get_request_body(self, req, connectors)?) + .set_body(types::RefundExecuteType::get_request_body(self, req, connectors)?) .build(); Ok(Some(request)) } @@ -444,7 +445,7 @@ impl .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .body(types::RefundSyncType::get_request_body(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body(self, req, connectors)?) .build(), )) } diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index d6d9281a4a05..e3aecb4b5695 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -25,6 +25,7 @@ pub enum ContentType { Json, FormUrlEncoded, FormData, + Xml, } fn default_request_headers() -> [(String, Maskable); 1] { @@ -37,12 +38,28 @@ fn default_request_headers() -> [(String, Maskable); 1] { pub struct Request { pub url: String, pub headers: Headers, - pub payload: Option>, pub method: Method, - pub content_type: Option, pub certificate: Option, pub certificate_key: Option, - pub form_data: Option, + pub body: Option, +} + +impl std::fmt::Debug for RequestContent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Json(_) => "JsonRequestBody", + Self::FormUrlEncoded(_) => "FormUrlEncodedRequestBody", + Self::FormData(_) => "FormDataRequestBody", + Self::Xml(_) => "XmlRequestBody", + }) + } +} + +pub enum RequestContent { + Json(Box), + FormUrlEncoded(Box), + FormData(reqwest::multipart::Form), + Xml(Box), } impl Request { @@ -51,16 +68,14 @@ impl Request { method, url: String::from(url), headers: std::collections::HashSet::new(), - payload: None, - content_type: None, certificate: None, certificate_key: None, - form_data: None, + body: None, } } - pub fn set_body(&mut self, body: String) { - self.payload = Some(body.into()); + pub fn set_body>(&mut self, body: T) { + self.body.replace(body.into()); } pub fn add_default_headers(&mut self) { @@ -71,10 +86,6 @@ impl Request { self.headers.insert((String::from(header), value)); } - pub fn add_content_type(&mut self, content_type: ContentType) { - self.content_type = Some(content_type); - } - pub fn add_certificate(&mut self, certificate: Option) { self.certificate = certificate; } @@ -82,22 +93,16 @@ impl Request { pub fn add_certificate_key(&mut self, certificate_key: Option) { self.certificate = certificate_key; } - - pub fn set_form_data(&mut self, form_data: reqwest::multipart::Form) { - self.form_data = Some(form_data); - } } #[derive(Debug)] pub struct RequestBuilder { pub url: String, pub headers: Headers, - pub payload: Option>, pub method: Method, - pub content_type: Option, pub certificate: Option, pub certificate_key: Option, - pub form_data: Option, + pub body: Option, } impl RequestBuilder { @@ -106,11 +111,9 @@ impl RequestBuilder { method: Method::Get, url: String::with_capacity(1024), headers: std::collections::HashSet::new(), - payload: None, - content_type: None, certificate: None, certificate_key: None, - form_data: None, + body: None, } } @@ -140,18 +143,8 @@ impl RequestBuilder { self } - pub fn form_data(mut self, form_data: Option) -> Self { - self.form_data = form_data; - self - } - - pub fn body(mut self, option_body: Option) -> Self { - self.payload = option_body.map(RequestBody::get_inner_value); - self - } - - pub fn content_type(mut self, content_type: ContentType) -> Self { - self.content_type = Some(content_type); + pub fn set_body>(mut self, body: T) -> Self { + self.body.replace(body.into()); self } @@ -170,11 +163,9 @@ impl RequestBuilder { method: self.method, url: self.url, headers: self.headers, - payload: self.payload, - content_type: self.content_type, certificate: self.certificate, certificate_key: self.certificate_key, - form_data: self.form_data, + body: self.body, } } } @@ -201,7 +192,15 @@ impl RequestBody { logger::info!(connector_request_body=?body); Ok(Self(Secret::new(encoder(body)?))) } - pub fn get_inner_value(request_body: Self) -> Secret { - request_body.0 + + pub fn get_inner_value(request_body: RequestContent) -> Secret { + match request_body { + RequestContent::Json(i) => serde_json::to_string(&i).unwrap_or_default().into(), + RequestContent::FormUrlEncoded(i) => { + serde_urlencoded::to_string(&i).unwrap_or_default().into() + } + RequestContent::Xml(i) => quick_xml::se::to_string(&i).unwrap_or_default().into(), + RequestContent::FormData(_) => String::new().into(), + } } } diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index c03de9a1aeed..23f207d63d59 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -19,6 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] bytes = { version = "1", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time"], optional = true } +erased-serde = "0.3.31" serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1.0.108", optional = true } subtle = "=2.4.1" diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs index d1845ee29033..944392e693ff 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -2,6 +2,7 @@ //! Serde-related. //! +pub use erased_serde::Serialize as ErasedSerialize; pub use serde::{de, ser, Deserialize, Serialize, Serializer}; use serde_json::{value::Serializer as JsonValueSerializer, Value}; @@ -99,20 +100,32 @@ pub fn masked_serialize(value: &T) -> Result because of Rust's "object safety" rules. /// In particular, the trait contains generic methods which cannot be made into a trait object. /// In this case we remove the generic for assuming the serialization to be of 2 types only raw json or masked json -pub trait ErasedMaskSerialize { +pub trait ErasedMaskSerialize: ErasedSerialize { /// Masked serialization. fn masked_serialize(&self) -> Result; - /// Normal serialization. - fn raw_serialize(&self) -> Result; } -impl ErasedMaskSerialize for T { +impl ErasedMaskSerialize for T { fn masked_serialize(&self) -> Result { masked_serialize(self) } +} + +impl<'a> Serialize for dyn ErasedMaskSerialize + 'a { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + erased_serde::serialize(self, serializer) + } +} - fn raw_serialize(&self) -> Result { - serde_json::to_value(self) +impl<'a> Serialize for dyn ErasedMaskSerialize + 'a + Send { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + erased_serde::serialize(self, serializer) } } diff --git a/crates/pm_auth/src/connector/plaid.rs b/crates/pm_auth/src/connector/plaid.rs index d25aba881d2d..dfcfbb7eddcc 100644 --- a/crates/pm_auth/src/connector/plaid.rs +++ b/crates/pm_auth/src/connector/plaid.rs @@ -3,8 +3,8 @@ pub mod transformers; use std::fmt::Debug; use common_utils::{ - ext_traits::{BytesExt, Encode}, - request::{Method, Request, RequestBody, RequestBuilder}, + ext_traits::BytesExt, + request::{Method, Request, RequestBuilder, RequestContent}, }; use error_stack::ResultExt; use masking::{Mask, Maskable}; @@ -121,14 +121,9 @@ impl ConnectorIntegration errors::CustomResult, errors::ConnectorError> { + ) -> errors::CustomResult { let req_obj = plaid::PlaidLinkTokenRequest::try_from(req)?; - let plaid_req = RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(plaid_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -146,7 +141,7 @@ impl ConnectorIntegration errors::CustomResult, errors::ConnectorError> { + ) -> errors::CustomResult { let req_obj = plaid::PlaidExchangeTokenRequest::try_from(req)?; - let plaid_req = RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(plaid_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -237,7 +227,7 @@ impl .headers(auth_types::PaymentAuthExchangeTokenType::get_headers( self, req, connectors, )?) - .body(auth_types::PaymentAuthExchangeTokenType::get_request_body( + .set_body(auth_types::PaymentAuthExchangeTokenType::get_request_body( self, req, )?) .build(), @@ -299,14 +289,9 @@ impl fn get_request_body( &self, req: &auth_types::BankDetailsRouterData, - ) -> errors::CustomResult, errors::ConnectorError> { + ) -> errors::CustomResult { let req_obj = plaid::PlaidBankAccountCredentialsRequest::try_from(req)?; - let plaid_req = RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(plaid_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -324,7 +309,9 @@ impl .headers(auth_types::PaymentAuthBankAccountDetailsType::get_headers( self, req, connectors, )?) - .body(auth_types::PaymentAuthBankAccountDetailsType::get_request_body(self, req)?) + .set_body( + auth_types::PaymentAuthBankAccountDetailsType::get_request_body(self, req)?, + ) .build(), )) } diff --git a/crates/pm_auth/src/types/api.rs b/crates/pm_auth/src/types/api.rs index 2416d0fee1de..3684e34ec052 100644 --- a/crates/pm_auth/src/types/api.rs +++ b/crates/pm_auth/src/types/api.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use common_utils::{ errors::CustomResult, - request::{Request, RequestBody}, + request::{Request, RequestContent}, }; use masking::Maskable; @@ -38,8 +38,8 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, - ) -> CustomResult, ConnectorError> { - Ok(None) + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(serde_json::json!(r#"{}"#)))) } fn build_request( diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 13324aa59a29..bc11d921ff4c 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -121,6 +121,7 @@ router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } erased-serde = "0.3.31" +quick-xml = { version = "0.31.0", features = ["serialize"] } rdkafka = "0.36.0" [build-dependencies] diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index f6384bf0a5c5..69a2c5364359 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -2,6 +2,7 @@ mod result_codes; pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as aci; @@ -20,7 +21,7 @@ use crate::{ self, api::{self, ConnectorCommon}, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -202,7 +203,7 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body( + .set_body(types::PaymentsSyncType::get_request_body( self, req, connectors, )?) .build(), @@ -284,7 +285,7 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { // encode only for for urlencoded things. let connector_router_data = aci::AciRouterData::try_from(( &self.get_currency_unit(), @@ -293,13 +294,8 @@ impl req, ))?; let connector_req = aci::AciPaymentsRequest::try_from(&connector_router_data)?; - let aci_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(aci_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -321,7 +317,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -393,14 +389,9 @@ impl &self, req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = aci::AciCancelRequest::try_from(req)?; - let aci_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(aci_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( &self, @@ -413,7 +404,7 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -489,7 +480,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = aci::AciRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -497,12 +488,7 @@ impl services::ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(body)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -518,7 +504,7 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let authorize_req = types::PaymentsAuthorizeRouterData::from(( req, types::PaymentsAuthorizeData::from(req), @@ -184,12 +185,7 @@ impl ))?; let connector_req = adyen::AdyenPaymentRequest::try_from(&connector_router_data)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -202,7 +198,7 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -309,7 +305,7 @@ impl &self, req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -317,12 +313,7 @@ impl req, ))?; let connector_req = adyen::AdyenCaptureRequest::try_from(&connector_router_data)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -337,7 +328,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(), @@ -405,71 +396,48 @@ impl &self, req: &types::RouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - // Adyen doesn't support PSync flow. We use PSync flow to fetch payment details, - // specifically the redirect URL that takes the user to their Payment page. In non-redirection flows, - // we rely on webhooks to obtain the payment status since there is no encoded data available. - // encoded_data only includes the redirect URL and is only relevant in redirection flows. - let encoded_value = req + ) -> CustomResult { + let encoded_data = req .request .encoded_data .clone() - .get_required_value("encoded_data"); - - match encoded_value { - Ok(encoded_data) => { - let adyen_redirection_type = serde_urlencoded::from_str::< - transformers::AdyenRedirectRequestTypes, - >(encoded_data.as_str()) - .into_report() - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - let redirection_request = match adyen_redirection_type { - adyen::AdyenRedirectRequestTypes::AdyenRedirection(req) => { - adyen::AdyenRedirectRequest { - details: adyen::AdyenRedirectRequestTypes::AdyenRedirection( - adyen::AdyenRedirection { - redirect_result: req.redirect_result, - type_of_redirection_result: None, - result_code: None, - }, - ), - } - } - adyen::AdyenRedirectRequestTypes::AdyenThreeDS(req) => { - adyen::AdyenRedirectRequest { - details: adyen::AdyenRedirectRequestTypes::AdyenThreeDS( - adyen::AdyenThreeDS { - three_ds_result: req.three_ds_result, - type_of_redirection_result: None, - result_code: None, - }, - ), - } - } - adyen::AdyenRedirectRequestTypes::AdyenRefusal(req) => { - adyen::AdyenRedirectRequest { - details: adyen::AdyenRedirectRequestTypes::AdyenRefusal( - adyen::AdyenRefusal { - payload: req.payload, - type_of_redirection_result: None, - result_code: None, - }, - ), - } - } - }; - - let adyen_request = types::RequestBody::log_and_get_request_body( - &redirection_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(Some(adyen_request)) + .get_required_value("encoded_data") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let adyen_redirection_type = serde_urlencoded::from_str::< + transformers::AdyenRedirectRequestTypes, + >(encoded_data.as_str()) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let connector_req = match adyen_redirection_type { + adyen::AdyenRedirectRequestTypes::AdyenRedirection(req) => { + adyen::AdyenRedirectRequest { + details: adyen::AdyenRedirectRequestTypes::AdyenRedirection( + adyen::AdyenRedirection { + redirect_result: req.redirect_result, + type_of_redirection_result: None, + result_code: None, + }, + ), + } } - Err(_) => Ok(None), - } + adyen::AdyenRedirectRequestTypes::AdyenThreeDS(req) => adyen::AdyenRedirectRequest { + details: adyen::AdyenRedirectRequestTypes::AdyenThreeDS(adyen::AdyenThreeDS { + three_ds_result: req.three_ds_result, + type_of_redirection_result: None, + result_code: None, + }), + }, + adyen::AdyenRedirectRequestTypes::AdyenRefusal(req) => adyen::AdyenRedirectRequest { + details: adyen::AdyenRedirectRequestTypes::AdyenRefusal(adyen::AdyenRefusal { + payload: req.payload, + type_of_redirection_result: None, + result_code: None, + }), + }, + }; + + Ok(RequestContent::Json(Box::new(connector_req))) } fn get_url( @@ -489,20 +457,30 @@ impl req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let request_body = self.get_request_body(req, connectors)?; - match request_body { - Some(_) => Ok(Some( + // Adyen doesn't support PSync flow. We use PSync flow to fetch payment details, + // specifically the redirect URL that takes the user to their Payment page. In non-redirection flows, + // we rely on webhooks to obtain the payment status since there is no encoded data available. + // encoded_data only includes the redirect URL and is only relevant in redirection flows. + if req + .request + .encoded_data + .clone() + .get_required_value("encoded_data") + .is_ok() + { + Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body( + .set_body(types::PaymentsSyncType::get_request_body( self, req, connectors, )?) .build(), - )), - None => Ok(None), + )) + } else { + Ok(None) } } @@ -599,7 +577,7 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -607,12 +585,7 @@ impl req, ))?; let connector_req = adyen::AdyenPaymentRequest::try_from(&connector_router_data)?; - let request_body = types::RequestBody::log_and_get_request_body( - &connector_req, - common_utils::ext_traits::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request_body)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -630,7 +603,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -717,15 +690,10 @@ impl &self, req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -743,7 +711,7 @@ impl .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -843,15 +811,10 @@ impl &self, req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = adyen::AdyenCancelRequest::try_from(req)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -864,7 +827,7 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -957,14 +920,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = adyen::AdyenPayoutCancelRequest::try_from(req)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -977,7 +935,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.destination_currency, @@ -1053,12 +1011,7 @@ impl services::ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1071,7 +1024,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.destination_currency, @@ -1148,12 +1101,7 @@ impl req, ))?; let connector_req = adyen::AdyenPayoutEligibilityRequest::try_from(&connector_router_data)?; - let adyen_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1170,7 +1118,7 @@ impl .headers(types::PayoutEligibilityType::get_headers( self, req, connectors, )?) - .body(types::PayoutEligibilityType::get_request_body( + .set_body(types::PayoutEligibilityType::get_request_body( self, req, connectors, )?) .build(); @@ -1252,7 +1200,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.destination_currency, @@ -1260,12 +1208,7 @@ impl services::ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1280,7 +1223,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = adyen::AdyenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -1362,12 +1305,7 @@ impl services::ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(adyen_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1383,7 +1321,7 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = airwallex::AirwallexIntentRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -278,7 +275,7 @@ impl .url(&types::PaymentsInitType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body( + .set_body(types::PaymentsInitType::get_request_body( self, req, connectors, )?) .build(), @@ -380,7 +377,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -388,12 +385,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -411,7 +403,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( &self, @@ -581,7 +568,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -646,16 +633,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = airwallex::AirwallexPaymentsCaptureRequest::try_from(req)?; - let airwallex_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -671,7 +652,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = airwallex::AirwallexPaymentsCancelRequest::try_from(req)?; - let airwallex_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( &self, @@ -787,7 +763,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = airwallex::AirwallexRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -845,12 +821,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(airwallex_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -865,7 +836,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -156,12 +156,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -177,7 +172,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = authorizedotnet::AuthorizedotnetCreateSyncRequest::try_from(req)?; - let sync_request = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(sync_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -265,7 +255,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -342,12 +332,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -369,7 +354,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = authorizedotnet::CancelOrCaptureTransactionRequest::try_from(req)?; - let authorizedotnet_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -455,7 +435,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -535,12 +515,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -555,7 +530,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -632,12 +607,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(sync_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -650,7 +620,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = authorizedotnet::AuthorizedotnetRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -730,12 +700,7 @@ impl let connector_req = authorizedotnet::PaypalConfirmRequest::try_from(&connector_router_data)?; - let authorizedotnet_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(authorizedotnet_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -753,7 +718,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index 19849763ed8e..a8e726a3e4f9 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as bambora; @@ -28,7 +29,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -175,15 +176,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let request = bambora::BamboraPaymentsRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = bambora::BamboraPaymentsRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -197,7 +193,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bambora::BamboraPaymentsCaptureRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -371,7 +362,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let request = bambora::BamboraPaymentsRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = bambora::BamboraPaymentsRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -468,7 +454,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bambora::BamboraRefundRequest::try_from(req)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -564,7 +545,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let request = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; + ) -> CustomResult { + let connector_req = bambora::BamboraThreedsContinueRequest::try_from(&req.request)?; - let bambora_req = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bambora_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -777,7 +753,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(); diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index a01ea72338c5..cac3bacc211d 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -3,6 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; @@ -27,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; pub const V_C_MERCHANT_ID: &str = "v-c-merchant-id"; @@ -134,10 +135,8 @@ where .skip(base_url.len() - 1) .collect(); let sha256 = self.generate_digest( - boa_req - .map_or("{}".to_string(), |s| { - types::RequestBody::get_inner_value(s).expose() - }) + types::RequestBody::get_inner_value(boa_req) + .expose() .as_bytes(), ); let signature = self.generate_signature( @@ -303,21 +302,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = + let connector_req = bankofamerica::BankOfAmericaPaymentsRequest::try_from(&connector_router_data)?; - let bankofamerica_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_payments_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -335,7 +329,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let connector_request = + let connector_req = bankofamerica::BankOfAmericaCaptureRequest::try_from(&connector_router_data)?; - let bankofamerica_capture_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_capture_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -501,7 +490,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request @@ -578,15 +567,10 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_void_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -600,7 +584,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = bankofamerica::BankOfAmericaRefundRequest::try_from(&connector_router_data)?; - let bankofamerica_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bankofamerica_req)) + let connector_req = + bankofamerica::BankOfAmericaRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -690,7 +670,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bitpay::BitpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = bitpay::BitpayPaymentsRequest::try_from(&connector_router_data)?; + let connector_req = bitpay::BitpayPaymentsRequest::try_from(&connector_router_data)?; - let bitpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bitpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -207,7 +202,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bluesnap::BluesnapVoidRequest::try_from(req)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -287,7 +283,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -452,12 +448,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -472,7 +463,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -562,7 +548,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -646,22 +632,12 @@ impl ConnectorIntegration { let connector_req = bluesnap::BluesnapPaymentsTokenRequest::try_from(&connector_router_data)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } _ => { let connector_req = bluesnap::BluesnapPaymentsRequest::try_from(&connector_router_data)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } } } @@ -681,7 +657,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -783,12 +759,7 @@ impl ))?; let connector_req = bluesnap::BluesnapCompletePaymentsRequest::try_from(&connector_router_data)?; - let bluesnap_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -805,7 +776,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -873,7 +844,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = bluesnap::BluesnapRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -881,12 +852,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(bluesnap_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -901,7 +867,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuPaymentsRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuPaymentsRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -234,7 +228,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuPsyncRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuPsyncRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -323,7 +312,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -403,7 +392,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuRefundRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuRefundRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -496,7 +480,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = boku::BokuRsyncRequest::try_from(req)?; - let boku_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_xml, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(boku_req)) + ) -> CustomResult { + let connector_req = boku::BokuRsyncRequest::try_from(req)?; + Ok(RequestContent::Xml(Box::new(connector_req))) } fn build_request( @@ -578,7 +557,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = braintree::BraintreeSessionRequest::try_from(req)?; - let braintree_session_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_session_request)) + ) -> CustomResult { + let connector_req = braintree::BraintreeSessionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -330,16 +324,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = - braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = braintree_graphql_transformers::BraintreeTokenRequest::try_from(req)?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -355,7 +343,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -448,7 +436,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version.clone(); let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -459,17 +447,12 @@ impl ConnectorIntegration { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeCaptureRequest::try_from( &connector_router_data, )?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => Err(errors::ConnectorError::NotImplemented( "get_request_body method".to_string(), @@ -493,7 +476,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreePSyncRequest::try_from(req)?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } - false => Ok(None), + false => Err(errors::ConnectorError::RequestEncodingFailed).into_report(), } } @@ -632,7 +610,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -788,25 +766,15 @@ impl ConnectorIntegration { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreePaymentsRequest::try_from( &connector_router_data, )?; - let braintree_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_payment_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => { - let connector_request = braintree::BraintreePaymentsRequest::try_from(req)?; - let braintree_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_payment_request)) + let connector_req = braintree::BraintreePaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } } } @@ -939,7 +907,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeCancelRequest::try_from(req)?; - let braintree_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } - false => Ok(None), + false => Err(errors::ConnectorError::RequestEncodingFailed).into_report(), } } @@ -1080,7 +1043,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( @@ -1091,25 +1054,15 @@ impl ConnectorIntegration { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeRefundRequest::try_from( connector_router_data, )?; - let braintree_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_refund_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => { - let connector_request = braintree::BraintreeRefundRequest::try_from(req)?; - let braintree_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_refund_request)) + let connector_req = braintree::BraintreeRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } } } @@ -1126,7 +1079,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreeRSyncRequest::try_from(req)?; - let braintree_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_refund_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } - false => Ok(None), + false => Err(errors::ConnectorError::RequestEncodingFailed).into_report(), } } @@ -1247,7 +1195,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = braintree_graphql_transformers::BraintreeRouterData::try_from(( &self.get_currency_unit(), @@ -1622,22 +1570,18 @@ impl let connector_api_version = &req.connector_api_version; match self.is_braintree_graphql_version(connector_api_version) { true => { - let connector_request = + let connector_req = braintree_graphql_transformers::BraintreePaymentsRequest::try_from( &connector_router_data, )?; - let braintree_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(braintree_payment_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } false => Err(errors::ConnectorError::NotImplemented( "get_request_body method".to_string(), ))?, } } + fn build_request( &self, req: &types::PaymentsCompleteAuthorizeRouterData, @@ -1655,7 +1599,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 6749f4189340..0b5634e0fba6 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -1,16 +1,16 @@ pub mod transformers; - use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use transformers as cashtocode; +use super::utils as connector_utils; use crate::{ configs::settings::{self}, - connector::{utils as connector_utils, utils as conn_utils}, core::errors::{self, CustomResult}, headers, services::{ @@ -23,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, domain, storage, ErrorResponse, Response, }, - utils::{self, ByteSliceExt, BytesExt}, + utils::{ByteSliceExt, BytesExt}, }; #[derive(Debug, Clone)] @@ -205,14 +205,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = cashtocode::CashtocodePaymentsRequest::try_from(req)?; - let cashtocode_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cashtocode_req)) + ) -> CustomResult { + let connector_req = cashtocode::CashtocodePaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -230,7 +225,7 @@ impl ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let base64_signature = conn_utils::get_header_key_value("authorization", request.headers)?; + let base64_signature = + connector_utils::get_header_key_value("authorization", request.headers)?; let signature = base64_signature.as_bytes().to_owned(); Ok(signature) } diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index 312a91196de7..bacb707e5648 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -29,7 +29,8 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, }, - utils::{self, BytesExt}, + utils, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -210,14 +211,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = checkout::TokenRequest::try_from(req)?; - let checkout_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -231,7 +227,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -318,7 +314,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -326,12 +322,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -347,7 +338,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -516,12 +507,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -542,7 +528,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = checkout::PaymentVoidRequest::try_from(req)?; - let checkout_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -622,7 +603,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = checkout::CheckoutRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -700,12 +681,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(body)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -720,7 +696,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let checkout_req = transformers::construct_file_upload_request(req.clone())?; - Ok(Some(checkout_req)) + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = transformers::construct_file_upload_request(req.clone())?; + Ok(RequestContent::FormData(connector_req)) } fn build_request( @@ -988,8 +965,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let checkout_req = checkout::Evidence::try_from(req)?; - let checkout_req_string = types::RequestBody::log_and_get_request_body( - &checkout_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(checkout_req_string)) + ) -> CustomResult { + let connector_req = checkout::Evidence::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1086,7 +1059,7 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body( + .set_body(types::SubmitEvidenceType::get_request_body( self, req, connectors, )?) .build(); diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index b294a4474f69..6753525ca474 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as coinbase; @@ -24,7 +24,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{BytesExt, Encode}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -186,14 +186,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = coinbase::CoinbasePaymentsRequest::try_from(req)?; - let coinbase_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(coinbase_payment_request)) + ) -> CustomResult { + let connector_req = coinbase::CoinbasePaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -210,7 +205,7 @@ impl ConnectorIntegration, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let api_method; - let payload = match self.get_request_body(req, connectors)? { - Some(val) => { - let body = types::RequestBody::get_inner_value(val).peek().to_owned(); - api_method = "POST".to_string(); - let md5_payload = crypto::Md5 - .generate_digest(body.as_bytes()) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - encode(md5_payload) - } - None => { - api_method = "GET".to_string(); - String::default() - } - }; + let api_method = self.get_http_method().to_string(); + let body = types::RequestBody::get_inner_value(self.get_request_body(req, connectors)?) + .peek() + .to_owned(); + let md5_payload = crypto::Md5 + .generate_digest(body.as_bytes()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let payload = encode(md5_payload); let now = date_time::date_as_yyyymmddthhmmssmmmz() .into_report() @@ -219,21 +213,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cryptopay::CryptopayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = - cryptopay::CryptopayPaymentsRequest::try_from(&connector_router_data)?; - let cryptopay_req = types::RequestBody::log_and_get_request_body( - &connector_request, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cryptopay_req)) + let connector_req = cryptopay::CryptopayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -251,7 +239,7 @@ impl ConnectorIntegration services::Method { + services::Method::Get + } + fn get_url( &self, req: &types::PaymentsSyncRouterData, diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 631b2f8c97ed..f74ab55595dd 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -3,6 +3,7 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; @@ -26,7 +27,7 @@ use crate::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -184,10 +185,8 @@ where .skip(base_url.len() - 1) .collect(); let sha256 = self.generate_digest( - cybersource_req - .map_or("{}".to_string(), |s| { - types::RequestBody::get_inner_value(s).expose() - }) + types::RequestBody::get_inner_value(cybersource_req) + .expose() .as_bytes(), ); let http_method = self.get_http_method(); @@ -278,14 +277,9 @@ impl &self, req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = cybersource::CybersourceZeroMandateRequest::try_from(req)?; - let cybersource_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_req)) + ) -> CustomResult { + let connector_req = cybersource::CybersourceZeroMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -299,7 +293,7 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -375,21 +369,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let connector_request = + let connector_req = cybersource::CybersourcePaymentsCaptureRequest::try_from(&connector_router_data)?; - let cybersource_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_payments_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -404,7 +393,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - )) - } fn build_request( &self, req: &types::PaymentsSyncRouterData, @@ -560,21 +539,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = + let connector_req = cybersource::CybersourcePaymentsRequest::try_from(&connector_router_data)?; - let cybersource_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_payments_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -591,7 +565,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - Ok(Some( - types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - )) + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(serde_json::json!({})))) } fn build_request( @@ -677,7 +648,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let connector_request = + let connector_req = cybersource::CybersourceRefundRequest::try_from(&connector_router_data)?; - let cybersource_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_refund_request)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -776,7 +742,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = cybersource::CybersourceRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -923,13 +886,7 @@ impl cybersource::CybersourcePaymentsIncrementalAuthorizationRequest::try_from( &connector_router_data, )?; - let cybersource_payments_incremental_authorization_request = - types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(cybersource_payments_incremental_authorization_request)) + Ok(RequestContent::Json(Box::new(connector_request))) } fn build_request( &self, @@ -946,7 +903,7 @@ impl .headers(types::IncrementalAuthorizationType::get_headers( self, req, connectors, )?) - .body(types::IncrementalAuthorizationType::get_request_body( + .set_body(types::IncrementalAuthorizationType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 28ae058286f0..155d422d895b 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -5,6 +5,7 @@ use std::fmt::Debug; use common_utils::{ crypto::{self, SignMessage}, date_time, + request::RequestContent, }; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; @@ -27,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -56,16 +57,11 @@ where connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let dlocal_req = match self.get_request_body(req, connectors)? { - Some(val) => val, - None => types::RequestBody::log_and_get_request_body("".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - }; + let dlocal_req = self.get_request_body(req, connectors)?; let date = date_time::date_as_yyyymmddthhmmssmmmz() .into_report() .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let auth = dlocal::DlocalAuthType::try_from(&req.connector_auth_type)?; let sign_req: String = format!( "{}{}{}", @@ -213,20 +209,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = dlocal::DlocalPaymentsRequest::try_from(&connector_router_data)?; - let dlocal_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dlocal_payments_request)) + let connector_req = dlocal::DlocalPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -244,7 +235,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = dlocal::DlocalPaymentsCaptureRequest::try_from(req)?; - let dlocal_payments_capture_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dlocal_payments_capture_request)) + ) -> CustomResult { + let connector_req = dlocal::DlocalPaymentsCaptureRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -397,7 +383,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = dlocal::DlocalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let connector_request = dlocal::DlocalRefundRequest::try_from(&connector_router_data)?; - let dlocal_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dlocal_refund_request)) + let connector_req = dlocal::DlocalRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -554,7 +535,7 @@ impl ConnectorIntegration &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; - let dummmy_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::>::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dummmy_payments_request)) + ) -> CustomResult { + let connector_req = transformers::DummyConnectorPaymentsRequest::::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -215,7 +211,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -353,7 +349,7 @@ impl &self, _req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -436,14 +432,9 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = transformers::DummyConnectorRefundRequest::try_from(req)?; - let dummmy_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(dummmy_refund_request)) + ) -> CustomResult { + let connector_req = transformers::DummyConnectorRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -458,7 +449,7 @@ impl ConnectorIntegration ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservCancelRequest::try_from(req)?; - let fiserv_payments_cancel_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_cancel_request)) + ) -> CustomResult { + let connector_req = fiserv::FiservCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -270,7 +264,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservSyncRequest::try_from(req)?; - let fiserv_payments_sync_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_sync_request)) + ) -> CustomResult { + let connector_req = fiserv::FiservSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -359,7 +348,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let connector_request = fiserv::FiservCaptureRequest::try_from(&router_obj)?; - let fiserv_payments_capture_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_capture_request)) + let connector_req = fiserv::FiservCaptureRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -443,7 +427,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = fiserv::FiservPaymentsRequest::try_from(&router_obj)?; - let fiserv_payments_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_payments_request)) + let connector_req = fiserv::FiservPaymentsRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -559,7 +538,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = fiserv::FiservRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let connector_request = fiserv::FiservRefundRequest::try_from(&router_obj)?; - let fiserv_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_refund_request)) + let connector_req = fiserv::FiservRefundRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -651,7 +625,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = fiserv::FiservSyncRequest::try_from(req)?; - let fiserv_sync_request = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_sync_request)) + ) -> CustomResult { + let connector_req = fiserv::FiservSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -734,7 +703,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::FortePaymentsRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -228,7 +224,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::ForteCaptureRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -385,7 +376,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::ForteCancelRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -469,7 +455,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = forte::ForteRefundRequest::try_from(req)?; - let forte_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(forte_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -553,7 +534,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let globalpay_req = types::RequestBody::log_and_get_request_body("{}".to_string(), Ok) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(serde_json::json!({})))) } fn build_request( @@ -188,7 +186,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -265,7 +263,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = GlobalpayRefreshTokenRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = GlobalpayRefreshTokenRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -389,7 +382,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = requests::GlobalpayCancelRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = requests::GlobalpayCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -551,14 +539,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = requests::GlobalpayCaptureRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = requests::GlobalpayCaptureRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -574,7 +557,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = GlobalpayPaymentsRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + ) -> CustomResult { + let connector_req = GlobalpayPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -668,7 +646,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = requests::GlobalpayRefundRequest::try_from(req)?; - let globalpay_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globalpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -758,7 +731,7 @@ impl ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let signature = conn_utils::get_header_key_value("x-gp-signature", request.headers)?; + let signature = connector_utils::get_header_key_value("x-gp-signature", request.headers)?; Ok(signature.as_bytes().to_vec()) } diff --git a/crates/router/src/connector/globepay.rs b/crates/router/src/connector/globepay.rs index 79704bb9530a..9870077b9b13 100644 --- a/crates/router/src/connector/globepay.rs +++ b/crates/router/src/connector/globepay.rs @@ -2,7 +2,10 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::crypto::{self, GenerateDigest}; +use common_utils::{ + crypto::{self, GenerateDigest}, + request::RequestContent, +}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use hex::encode; @@ -22,7 +25,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -190,14 +193,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = globepay::GlobepayPaymentsRequest::try_from(req)?; - let globepay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globepay_req)) + ) -> CustomResult { + let connector_req = globepay::GlobepayPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -215,7 +213,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = globepay::GlobepayRefundRequest::try_from(req)?; - let globepay_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(globepay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -392,7 +385,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = gocardless::GocardlessCustomerRequest::try_from(req)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + ) -> CustomResult { + let connector_req = gocardless::GocardlessCustomerRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -180,7 +175,7 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body( + .set_body(types::ConnectorCustomerType::get_request_body( self, req, connectors, )?) .build(), @@ -253,14 +248,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = gocardless::GocardlessBankAccountRequest::try_from(req)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + ) -> CustomResult { + let connector_req = gocardless::GocardlessBankAccountRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -274,7 +264,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -374,14 +364,9 @@ impl &self, req: &types::SetupMandateRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = gocardless::GocardlessMandateRequest::try_from(req)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + ) -> CustomResult { + let connector_req = gocardless::GocardlessMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -397,7 +382,7 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -458,20 +443,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = gocardless::GocardlessPaymentsRequest::try_from(&connector_router_data)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + let connector_req = + gocardless::GocardlessPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -489,7 +470,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = gocardless::GocardlessRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = gocardless::GocardlessRefundRequest::try_from(&connector_router_data)?; - let gocardless_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(gocardless_req)) + let connector_req = gocardless::GocardlessRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -654,7 +630,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = helcim::HelcimVerifyRequest::try_from(req)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -216,7 +212,7 @@ impl .url(&types::SetupMandateType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -275,20 +271,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = helcim::HelcimPaymentsRequest::try_from(&connector_router_data)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + let connector_req = helcim::HelcimPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -306,7 +297,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -457,12 +448,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -478,7 +464,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = helcim::HelcimVoidRequest::try_from(req)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + ) -> CustomResult { + let connector_req = helcim::HelcimVoidRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -557,7 +538,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = helcim::HelcimRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = helcim::HelcimRefundRequest::try_from(&connector_router_data)?; - let helcim_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(helcim_req)) + let connector_req = helcim::HelcimRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -642,7 +618,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = iatapay::IatapayAuthUpdateRequest::try_from(req)?; - let iatapay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(iatapay_req)) + ) -> CustomResult { + let connector_req = iatapay::IatapayAuthUpdateRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -201,7 +196,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = iatapay::IatapayPaymentsRequest::try_from(&connector_router_data)?; - let iatapay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(iatapay_req)) + let connector_req = iatapay::IatapayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -313,7 +303,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = iatapay::IatapayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.payment_amount, req, ))?; - let req_obj = iatapay::IatapayRefundRequest::try_from(&connector_router_data)?; - let iatapay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(iatapay_req)) + let connector_req = iatapay::IatapayRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -499,7 +484,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = klarna::KlarnaSessionRequest::try_from(req)?; // encode only for for urlencoded things. - let klarna_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(klarna_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -180,7 +176,7 @@ impl .headers(types::PaymentsSessionType::get_headers( self, req, connectors, )?) - .body(types::PaymentsSessionType::get_request_body( + .set_body(types::PaymentsSessionType::get_request_body( self, req, connectors, )?) .build(), @@ -417,7 +413,7 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = klarna::KlarnaRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -425,12 +421,7 @@ impl req, ))?; let connector_req = klarna::KlarnaPaymentsRequest::try_from(&connector_router_data)?; - let klarna_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(klarna_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -448,7 +439,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index 4e610003de31..023ab3da7acc 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as mollie; @@ -24,7 +25,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -152,14 +153,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = mollie::MollieCardTokenRequest::try_from(req)?; - let mollie_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(mollie_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -172,7 +168,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -238,20 +234,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = mollie::MolliePaymentsRequest::try_from(&router_obj)?; - let mollie_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(mollie_req)) + let connector_req = mollie::MolliePaymentsRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -269,7 +260,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = mollie::MollieRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = mollie::MollieRefundRequest::try_from(&router_obj)?; - let mollie_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(mollie_req)) + let connector_req = mollie::MollieRefundRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -463,7 +449,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = multisafepay::MultisafepayPaymentsRequest::try_from(&connector_router_data)?; - let multisafepay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(multisafepay_req)) + let connector_req = + multisafepay::MultisafepayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -295,7 +292,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = multisafepay::MultisafepayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = multisafepay::MultisafepayRefundRequest::try_from(&connector_req)?; + let connector_req = multisafepay::MultisafepayRefundRequest::try_from(&connector_req)?; - let multisafepay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(multisafepay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -397,7 +389,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nexinets::NexinetsPaymentsRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + ) -> CustomResult { + let connector_req = nexinets::NexinetsPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -227,7 +223,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -390,7 +381,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nexinets::NexinetsCaptureOrVoidRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -474,7 +460,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = nexinets::NexinetsRefundRequest::try_from(req)?; - let nexinets_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nexinets_req)) + ) -> CustomResult { + let connector_req = nexinets::NexinetsRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -562,7 +543,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiPaymentsRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -164,7 +157,7 @@ impl .method(services::Method::Post) .url(&types::SetupMandateType::get_url(self, req, connectors)?) .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .body(types::SetupMandateType::get_request_body( + .set_body(types::SetupMandateType::get_request_body( self, req, connectors, )?) .build(), @@ -217,7 +210,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -225,12 +218,7 @@ impl ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -247,7 +235,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiSyncRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -320,7 +303,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -378,12 +361,7 @@ impl ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -398,7 +376,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiCancelRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -471,7 +444,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = nmi::NmiRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -530,12 +503,7 @@ impl ConnectorIntegration::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -550,7 +518,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = nmi::NmiSyncRequest::try_from(req)?; - let nmi_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(nmi_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -621,7 +584,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = noon::NoonPaymentsRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + ) -> CustomResult { + let connector_req = noon::NoonPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -237,7 +232,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + ) -> CustomResult { + let connector_req = noon::NoonPaymentsActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -384,7 +374,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = noon::NoonPaymentsCancelRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -462,7 +447,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = noon::NoonPaymentsActionRequest::try_from(req)?; - let noon_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(noon_req)) + ) -> CustomResult { + let connector_req = noon::NoonPaymentsActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -540,7 +520,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let meta: nuvei::NuveiMeta = utils::to_connector_meta(req.request.connector_meta.clone())?; - let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, meta.session_token))?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -176,7 +172,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -237,14 +233,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -257,7 +248,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentSyncRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -344,7 +330,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -429,7 +410,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -592,7 +568,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiSessionRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiSessionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -683,7 +654,7 @@ impl .headers(types::PaymentsPreAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsPreAuthorizeType::get_request_body( + .set_body(types::PaymentsPreAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -742,15 +713,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentsRequest::try_from((req, req.get_session_token()?))?; - Ok(Some(req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -764,7 +730,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = nuvei::NuveiPaymentFlowRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - common_utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = nuvei::NuveiPaymentFlowRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -846,7 +807,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = opayo::OpayoPaymentsRequest::try_from(req)?; - let opayo_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(opayo_req)) + ) -> CustomResult { + let connector_req = opayo::OpayoPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -200,7 +196,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -337,7 +333,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = opayo::OpayoRefundRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = opayo::OpayoRefundRequest::try_from(req)?; - let opayo_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(opayo_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -421,7 +412,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = opennode::OpennodeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = opennode::OpennodePaymentsRequest::try_from(&connector_router_data)?; - let opennode_req = types::RequestBody::log_and_get_request_body( - &req_obj, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(opennode_req)) + let connector_req = opennode::OpennodePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -205,7 +200,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { let auth = payeezy::PayeezyAuthType::try_from(&req.connector_auth_type)?; - let option_request_payload = self.get_request_body(req, connectors)?; - let request_payload = option_request_payload.map_or("{}".to_string(), |payload| { - types::RequestBody::get_inner_value(payload).expose() - }); + let request_payload = + types::RequestBody::get_inner_value(self.get_request_body(req, connectors)?).expose(); let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .ok() @@ -202,14 +201,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payeezy_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -222,7 +216,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let req_obj = payeezy::PayeezyCaptureOrVoidRequest::try_from(&router_obj)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(&router_obj)?; - Ok(Some(payeezy_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -330,7 +319,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = payeezy::PayeezyPaymentsRequest::try_from(&router_obj)?; + let connector_req = payeezy::PayeezyPaymentsRequest::try_from(&router_obj)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payeezy_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -430,7 +414,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let router_obj = payeezy::PayeezyRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = payeezy::PayeezyRefundRequest::try_from(&router_obj)?; - let payeezy_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payeezy_req)) + let connector_req = payeezy::PayeezyRefundRequest::try_from(&router_obj)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -525,7 +504,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = payme::CaptureBuyerRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = payme::CaptureBuyerRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -175,7 +170,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -258,18 +253,13 @@ impl &self, req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let amount = req.request.get_amount()?; let currency = req.request.get_currency()?; let connector_router_data = payme::PaymeRouterData::try_from((&self.get_currency_unit(), currency, amount, req))?; - let req_obj = payme::GenerateSaleRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::GenerateSaleRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -287,7 +277,7 @@ impl .url(&types::PaymentsPreProcessingType::get_url( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -384,14 +374,9 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::Pay3dsRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + ) -> CustomResult { + let connector_req = payme::Pay3dsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, @@ -408,7 +393,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -487,20 +472,15 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = payme::PaymePaymentRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::PaymePaymentRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -518,7 +498,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::PaymeQuerySaleRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + ) -> CustomResult { + let connector_req = payme::PaymeQuerySaleRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -605,7 +580,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let req_obj = payme::PaymentCaptureRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::PaymentCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -701,7 +671,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = payme::PaymeRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = payme::PaymeRefundRequest::try_from(&connector_router_data)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + let connector_req = payme::PaymeRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -809,7 +774,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::PaymeQueryTransactionRequest::try_from(req)?; - let payme_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payme_req)) + ) -> CustomResult { + let connector_req = payme::PaymeQueryTransactionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -893,7 +853,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = paypal::PaypalAuthUpdateRequest::try_from(req)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = paypal::PaypalAuthUpdateRequest::try_from(req)?; - Ok(Some(paypal_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -342,7 +337,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -434,13 +429,8 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + let connector_req = paypal::PaypalPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -457,7 +447,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -919,12 +909,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -939,7 +924,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = paypal::PaypalRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = paypal::PaypalRefundRequest::try_from(&connector_router_data)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + let connector_req = paypal::PaypalRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -1102,7 +1082,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?; - let paypal_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(paypal_req)) + ) -> CustomResult { + let connector_req = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 8ac3b63f72a3..85f55e11adbe 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -2,6 +2,7 @@ pub mod transformers; use std::fmt::Debug; +use common_utils::request::RequestContent; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -22,7 +23,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -246,15 +247,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = payu::PayuAuthUpdateRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = payu::PayuAuthUpdateRequest::try_from(req)?; - Ok(Some(payu_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -268,7 +264,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payu::PayuPaymentsCaptureRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payu_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -447,7 +438,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payu::PayuPaymentsRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payu_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -550,7 +536,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = payu::PayuRefundRequest::try_from(req)?; - let payu_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(payu_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -640,7 +621,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = placetopay::PlacetopayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -173,12 +174,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(placetopay_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -196,7 +192,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -333,7 +329,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = placetopay::PlacetopayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -404,12 +400,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(placetopay_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -424,7 +415,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzPaymentsRequest::try_from(req)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -221,7 +216,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let redirect_payload: powertranz::RedirectResponsePayload = req .request .get_redirect_response_payload()? .parse_value("PowerTranz RedirectResponsePayload") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; let spi_token = format!(r#""{}""#, redirect_payload.spi_token); - let powertranz_req = - types::RequestBody::log_and_get_request_body(&spi_token, |spi_token| { - Ok(spi_token.to_string()) - }) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + Ok(RequestContent::Json(Box::new(spi_token))) } fn build_request( @@ -313,7 +303,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -377,14 +367,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzBaseRequest::try_from(&req.request)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -400,7 +385,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzBaseRequest::try_from(&req.request)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzBaseRequest::try_from(&req.request)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn handle_response( @@ -491,7 +471,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = powertranz::PowertranzBaseRequest::try_from(req)?; - let powertranz_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(powertranz_req)) + ) -> CustomResult { + let connector_req = powertranz::PowertranzBaseRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -555,7 +530,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = prophetpay::ProphetpayTokenRequest::try_from(&connector_router_data)?; + let connector_req = prophetpay::ProphetpayTokenRequest::try_from(&connector_router_data)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -208,7 +204,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = prophetpay::ProphetpayCompleteRequest::try_from(&connector_router_data)?; + let connector_req = + prophetpay::ProphetpayCompleteRequest::try_from(&connector_router_data)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -309,7 +301,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -373,15 +365,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = prophetpay::ProphetpaySyncRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = prophetpay::ProphetpaySyncRequest::try_from(req)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -395,7 +382,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = prophetpay::ProphetpayVoidRequest::try_from(req)?; + ) -> CustomResult { + let connector_req =prophetpay::ProphetpayVoidRequest::try_from(req)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } */ @@ -540,20 +522,15 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = prophetpay::ProphetpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = prophetpay::ProphetpayRefundRequest::try_from(&connector_router_data)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + let connector_req = prophetpay::ProphetpayRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -568,7 +545,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = prophetpay::ProphetpayRefundSyncRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = prophetpay::ProphetpayRefundSyncRequest::try_from(req)?; - let prophetpay_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(prophetpay_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -652,7 +624,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = rapyd::RapydPaymentsRequest::try_from(&connector_router_data)?; - let rapyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(rapyd_req)) + let connector_req = rapyd::RapydPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -214,8 +209,7 @@ impl let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12); let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?; - let body = types::PaymentsAuthorizeType::get_request_body(self, req, connectors)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let body = types::PaymentsAuthorizeType::get_request_body(self, req, connectors)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = self.generate_signature(&auth, "post", "/v1/payments", &req_body, ×tamp, &salt)?; @@ -235,7 +229,7 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(); @@ -498,20 +492,15 @@ impl &self, req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount_to_capture, req, ))?; - let req_obj = rapyd::CaptureRequest::try_from(&connector_router_data)?; - let rapyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(rapyd_req)) + let connector_req = rapyd::CaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -527,8 +516,7 @@ impl "/v1/payments/{}/capture", req.request.connector_transaction_id ); - let body = types::PaymentsCaptureType::get_request_body(self, req, connectors)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let body = types::PaymentsCaptureType::get_request_body(self, req, connectors)?; let req_body = types::RequestBody::get_inner_value(body).expose(); let signature = self.generate_signature(&auth, "post", &url_path, &req_body, ×tamp, &salt)?; @@ -546,7 +534,7 @@ impl self, req, connectors, )?) .headers(headers) - .body(types::PaymentsCaptureType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(); @@ -639,21 +627,16 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = rapyd::RapydRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = rapyd::RapydRefundRequest::try_from(&connector_router_data)?; - let rapyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let connector_req = rapyd::RapydRefundRequest::try_from(&connector_router_data)?; - Ok(Some(rapyd_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -664,8 +647,7 @@ impl services::ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let base64_signature = conn_utils::get_header_key_value("signature", request.headers)?; + let base64_signature = connector_utils::get_header_key_value("signature", request.headers)?; let signature = consts::BASE64_ENGINE_URL_SAFE .decode(base64_signature.as_bytes()) .into_report() @@ -765,11 +747,11 @@ impl api::IncomingWebhook for Rapyd { merchant_id: &str, connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let host = conn_utils::get_header_key_value("host", request.headers)?; + let host = connector_utils::get_header_key_value("host", request.headers)?; let connector = self.id(); let url_path = format!("https://{host}/webhooks/{merchant_id}/{connector}"); - let salt = conn_utils::get_header_key_value("salt", request.headers)?; - let timestamp = conn_utils::get_header_key_value("timestamp", request.headers)?; + let salt = connector_utils::get_header_key_value("salt", request.headers)?; + let timestamp = connector_utils::get_header_key_value("timestamp", request.headers)?; let stringify_auth = String::from_utf8(connector_webhook_secrets.secret.to_vec()) .into_report() .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index e34d12def02a..d4c24175dc41 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -1,6 +1,8 @@ pub mod transformers; use std::fmt::Debug; +#[cfg(feature = "frm")] +use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use ring::hmac; @@ -22,7 +24,7 @@ use crate::{ use crate::{ services, types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -59,9 +61,7 @@ where let auth: riskified::RiskifiedAuthType = riskified::RiskifiedAuthType::try_from(&req.connector_auth_type)?; - let riskified_req = self - .get_request_body(req, connectors)? - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + let riskified_req = self.get_request_body(req, connectors)?; let binding = types::RequestBody::get_inner_value(riskified_req); let payload = binding.peek(); @@ -157,14 +157,9 @@ impl &self, req: &frm_types::FrmCheckoutRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = riskified::RiskifiedPaymentsCheckoutRequest::try_from(req)?; - let riskified_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(riskified_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -180,7 +175,7 @@ impl .headers(frm_types::FrmCheckoutType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmCheckoutType::get_request_body( + .set_body(frm_types::FrmCheckoutType::get_request_body( self, req, connectors, )?) .build(), @@ -276,25 +271,15 @@ impl &self, req: &frm_types::FrmTransactionRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { match req.is_payment_successful() { Some(false) => { let req_obj = riskified::TransactionFailedRequest::try_from(req)?; - let riskified_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(riskified_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } Some(true) => { let req_obj = riskified::TransactionSuccessRequest::try_from(req)?; - let riskified_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(riskified_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } None => Err(errors::ConnectorError::FlowNotSupported { flow: "Transaction".to_owned(), @@ -318,7 +303,7 @@ impl .headers(frm_types::FrmTransactionType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmTransactionType::get_request_body( + .set_body(frm_types::FrmTransactionType::get_request_body( self, req, connectors, )?) .build(), @@ -392,14 +377,9 @@ impl &self, req: &frm_types::FrmFulfillmentRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = riskified::RiskifiedFullfillmentRequest::try_from(req)?; - let riskified_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(riskified_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -417,7 +397,7 @@ impl .headers(frm_types::FrmFulfillmentType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmFulfillmentType::get_request_body( + .set_body(frm_types::FrmFulfillmentType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index dfb4a7de0811..f1f00fd4b857 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as shift4; @@ -26,7 +26,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -188,14 +188,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = shift4::Shift4PaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } async fn execute_pretasks( @@ -251,7 +246,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = shift4::Shift4PaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -498,10 +488,9 @@ impl services::RequestBuilder::new() .method(services::Method::Post) .url(&types::PaymentsInitType::get_url(self, req, connectors)?) - .content_type(request::ContentType::FormUrlEncoded) .attach_default_headers() .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) - .body(types::PaymentsInitType::get_request_body( + .set_body(types::PaymentsInitType::get_request_body( self, req, connectors, )?) .build(), @@ -564,14 +553,9 @@ impl &self, req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = shift4::Shift4PaymentsRequest::try_from(req)?; - let req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(req)) + ) -> CustomResult { + let connector_req = shift4::Shift4PaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -588,7 +572,7 @@ impl .headers(types::PaymentsCompleteAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCompleteAuthorizeType::get_request_body( + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -645,14 +629,9 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = shift4::Shift4RefundRequest::try_from(req)?; - let shift4_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(shift4_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -667,7 +646,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = signifyd::SignifydPaymentsSaleRequest::try_from(req)?; - let signifyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(signifyd_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -231,7 +228,7 @@ impl .url(&frm_types::FrmSaleType::get_url(self, req, connectors)?) .attach_default_headers() .headers(frm_types::FrmSaleType::get_headers(self, req, connectors)?) - .body(frm_types::FrmSaleType::get_request_body( + .set_body(frm_types::FrmSaleType::get_request_body( self, req, connectors, )?) .build(), @@ -297,14 +294,9 @@ impl &self, req: &frm_types::FrmCheckoutRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = signifyd::SignifydPaymentsCheckoutRequest::try_from(req)?; - let signifyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(signifyd_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -320,7 +312,7 @@ impl .headers(frm_types::FrmCheckoutType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmCheckoutType::get_request_body( + .set_body(frm_types::FrmCheckoutType::get_request_body( self, req, connectors, )?) .build(), @@ -386,14 +378,9 @@ impl &self, req: &frm_types::FrmTransactionRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = signifyd::SignifydPaymentsTransactionRequest::try_from(req)?; - let signifyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(signifyd_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -411,7 +398,7 @@ impl .headers(frm_types::FrmTransactionType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmTransactionType::get_request_body( + .set_body(frm_types::FrmTransactionType::get_request_body( self, req, connectors, )?) .build(), @@ -477,14 +464,9 @@ impl &self, req: &frm_types::FrmFulfillmentRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = signifyd::FrmFullfillmentSignifydRequest::try_from(req)?; - let signifyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(signifyd_req)) + Ok(RequestContent::Json(Box::new(req_obj.clone()))) } fn build_request( @@ -502,7 +484,7 @@ impl .headers(frm_types::FrmFulfillmentType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmFulfillmentType::get_request_body( + .set_body(frm_types::FrmFulfillmentType::get_request_body( self, req, connectors, )?) .build(), @@ -568,14 +550,9 @@ impl &self, req: &frm_types::FrmRecordReturnRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let req_obj = signifyd::SignifydPaymentsRecordReturnRequest::try_from(req)?; - let signifyd_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(signifyd_req)) + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -593,7 +570,7 @@ impl .headers(frm_types::FrmRecordReturnType::get_headers( self, req, connectors, )?) - .body(frm_types::FrmRecordReturnType::get_request_body( + .set_body(frm_types::FrmRecordReturnType::get_request_body( self, req, connectors, )?) .build(), diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index 1f1dee6b9e1b..f9da19776e45 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use api_models::enums; use base64::Engine; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as square; @@ -28,7 +28,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -248,15 +248,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = square::SquareTokenRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = square::SquareTokenRequest::try_from(req)?; - let square_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(square_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -270,7 +265,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -418,15 +413,10 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = square::SquarePaymentsRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = square::SquarePaymentsRequest::try_from(req)?; - let square_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(square_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -444,7 +434,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = square::SquareRefundRequest::try_from(req)?; - let square_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(square_req)) + ) -> CustomResult { + let connector_req = square::SquareRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -733,7 +718,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = stax::StaxCustomerRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = stax::StaxCustomerRequest::try_from(req)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -189,7 +184,7 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body( + .set_body(types::ConnectorCustomerType::get_request_body( self, req, connectors, )?) .build(), @@ -255,15 +250,10 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stax::StaxTokenRequest::try_from(req)?; + ) -> CustomResult { + let connector_req = stax::StaxTokenRequest::try_from(req)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -277,7 +267,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -359,21 +349,16 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = stax::StaxPaymentsRequest::try_from(&connector_router_data)?; + let connector_req = stax::StaxPaymentsRequest::try_from(&connector_router_data)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -391,7 +376,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -532,12 +517,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -553,7 +533,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = stax::StaxRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = stax::StaxRefundRequest::try_from(&connector_router_data)?; - let stax_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stax_req)) + let connector_req = stax::StaxRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -715,7 +690,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req = stripe::StripeCreditTransferSourceRequest::try_from(req)?; - let pre_processing_request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = stripe::StripeCreditTransferSourceRequest::try_from(req)?; - Ok(Some(pre_processing_request)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -171,7 +167,7 @@ impl .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -273,14 +269,9 @@ impl &self, req: &types::ConnectorCustomerRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::CustomerRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::CustomerRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -298,7 +289,7 @@ impl .headers(types::ConnectorCustomerType::get_headers( self, req, connectors, )?) - .body(types::ConnectorCustomerType::get_request_body( + .set_body(types::ConnectorCustomerType::get_request_body( self, req, connectors, )?) .build(), @@ -404,14 +395,9 @@ impl &self, req: &types::TokenizationRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::TokenRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::TokenRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -425,7 +411,7 @@ impl .url(&types::TokenizationType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::TokenizationType::get_headers(self, req, connectors)?) - .body(types::TokenizationType::get_request_body( + .set_body(types::TokenizationType::get_request_body( self, req, connectors, )?) .build(), @@ -537,14 +523,9 @@ impl &self, req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::CaptureRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::CaptureRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -560,7 +541,7 @@ impl .headers(types::PaymentsCaptureType::get_headers( self, req, connectors, )?) - .body(types::PaymentsCaptureType::get_request_body( + .set_body(types::PaymentsCaptureType::get_request_body( self, req, connectors, )?) .build(), @@ -685,7 +666,7 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .body(types::PaymentsSyncType::get_request_body( + .set_body(types::PaymentsSyncType::get_request_body( self, req, connectors, )?) .build(), @@ -828,19 +809,14 @@ impl &self, req: &types::PaymentsAuthorizeRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { match &req.request.payment_method_data { api_models::payments::PaymentMethodData::BankTransfer(bank_transfer_data) => { stripe::get_bank_transfer_request_data(req, bank_transfer_data.deref()) } _ => { - let req = stripe::PaymentIntentRequest::try_from(req)?; - let request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request)) + let connector_req = stripe::PaymentIntentRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } } } @@ -860,7 +836,7 @@ impl .headers(types::PaymentsAuthorizeType::get_headers( self, req, connectors, )?) - .body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::PaymentsAuthorizeType::get_request_body( self, req, connectors, )?) .build(), @@ -972,14 +948,9 @@ impl &self, req: &types::PaymentsCancelRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::CancelRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::CancelRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -992,7 +963,7 @@ impl .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) - .body(types::PaymentsVoidType::get_request_body( + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(); @@ -1110,14 +1081,9 @@ impl types::PaymentsResponseData, >, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req = stripe::SetupIntentRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::SetupIntentRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -1135,7 +1101,7 @@ impl .url(&Verify::get_url(self, req, connectors)?) .attach_default_headers() .headers(Verify::get_headers(self, req, connectors)?) - .body(Verify::get_request_body(self, req, connectors)?) + .set_body(Verify::get_request_body(self, req, connectors)?) .build(), )) } @@ -1248,14 +1214,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let connector_request = stripe::RefundRequest::try_from(req)?; - let stripe_req = types::RequestBody::log_and_get_request_body( - &connector_request, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req)) + ) -> CustomResult { + let connector_req = stripe::RefundRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -1270,7 +1231,7 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { - let stripe_req = transformers::construct_file_upload_request(req.clone())?; - Ok(Some(stripe_req)) + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = transformers::construct_file_upload_request(req.clone())?; + Ok(RequestContent::FormData(connector_req)) } fn build_request( @@ -1521,8 +1483,9 @@ impl .url(&types::UploadFileType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::UploadFileType::get_headers(self, req, connectors)?) - .form_data(types::UploadFileType::get_request_form_data(self, req)?) - .content_type(services::request::ContentType::FormData) + .set_body(types::UploadFileType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -1734,14 +1697,9 @@ impl &self, req: &types::SubmitEvidenceRouterData, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let stripe_req = stripe::Evidence::try_from(req)?; - let stripe_req_string = types::RequestBody::log_and_get_request_body( - &stripe_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(stripe_req_string)) + ) -> CustomResult { + let connector_req = stripe::Evidence::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -1756,7 +1714,7 @@ impl .headers(types::SubmitEvidenceType::get_headers( self, req, connectors, )?) - .body(types::SubmitEvidenceType::get_request_body( + .set_body(types::SubmitEvidenceType::get_request_body( self, req, connectors, )?) .build(); diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index fad029c1c9db..3a28f777907f 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -5,6 +5,7 @@ use common_utils::{ errors::CustomResult, ext_traits::{ByteSliceExt, BytesExt}, pii::{self, Email}, + request::RequestContent, }; use data_models::mandates::AcceptanceType; use error_stack::{IntoReport, ResultExt}; @@ -26,7 +27,7 @@ use crate::{ storage::enums, transformers::{ForeignFrom, ForeignTryFrom}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; pub struct StripeAuthType { @@ -3424,26 +3425,16 @@ pub struct StripeGpayToken { pub fn get_bank_transfer_request_data( req: &types::PaymentsAuthorizeRouterData, bank_transfer_data: &api_models::payments::BankTransferData, -) -> CustomResult, errors::ConnectorError> { +) -> CustomResult { match bank_transfer_data { api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::MultibancoBankTransfer { .. } => { let req = ChargesRequest::try_from(req)?; - let request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request)) + Ok(RequestContent::FormUrlEncoded(Box::new(req))) } _ => { let req = PaymentIntentRequest::try_from(req)?; - let request = types::RequestBody::log_and_get_request_body( - &req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(request)) + Ok(RequestContent::FormUrlEncoded(Box::new(req))) } } } diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 9d9d998aa18c..c618357ffb5d 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -3,7 +3,9 @@ pub mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::{crypto, errors::ReportSwitchExt, ext_traits::ByteSliceExt}; +use common_utils::{ + crypto, errors::ReportSwitchExt, ext_traits::ByteSliceExt, request::RequestContent, +}; use error_stack::{IntoReport, Report, ResultExt}; use masking::PeekInterface; use transformers as trustpay; @@ -240,14 +242,9 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = trustpay::TrustpayAuthUpdateRequest::try_from(req)?; - let trustpay_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(trustpay_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -261,7 +258,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let currency = req.request.get_currency()?; let amount = req .request @@ -447,14 +444,9 @@ impl amount, req, ))?; - let create_intent_req = + let connector_req = trustpay::TrustpayCreateIntentRequest::try_from(&connector_router_data)?; - let trustpay_req = types::RequestBody::log_and_get_request_body( - &create_intent_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(trustpay_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -472,7 +464,7 @@ impl .url(&types::PaymentsPreProcessingType::get_url( self, req, connectors, )?) - .body(types::PaymentsPreProcessingType::get_request_body( + .set_body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -551,7 +543,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let amount = req .request .surcharge_details @@ -565,21 +557,12 @@ impl ConnectorIntegration { - types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)? + Ok(RequestContent::Json(Box::new(connector_req))) } - _ => types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - }; - Ok(Some(trustpay_req_string)) + _ => Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))), + } } fn build_request( @@ -597,7 +580,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -677,22 +660,12 @@ impl ConnectorIntegration { - types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)? + Ok(RequestContent::Json(Box::new(connector_req))) } - _ => - types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?, - }; - Ok(Some(trustpay_req_string)) + _ => Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))), + } } fn build_request( @@ -707,7 +680,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysPaymentsRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -169,7 +165,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysSyncRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -251,7 +242,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = tsys::TsysPaymentsCaptureRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -335,7 +321,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysPaymentsCancelRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysPaymentsCancelRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -414,7 +395,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysRefundRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -495,7 +471,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = tsys::TsysSyncRequest::try_from(req)?; - let tsys_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(tsys_req)) + ) -> CustomResult { + let connector_req = tsys::TsysSyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -575,7 +546,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let req_obj = volt::VoltAuthUpdateRequest::try_from(req)?; - let volt_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::url_encode, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + ) -> CustomResult { + let connector_req = volt::VoltAuthUpdateRequest::try_from(req)?; - Ok(Some(volt_req)) + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } fn build_request( @@ -198,7 +194,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = volt::VoltPaymentsRequest::try_from(&connector_router_data)?; - let volt_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(volt_req)) + let connector_req = volt::VoltPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -298,7 +289,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) } @@ -443,7 +434,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = volt::VoltRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = volt::VoltRefundRequest::try_from(&connector_router_data)?; - let volt_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(volt_req)) + let connector_req = volt::VoltRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -536,7 +522,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WisePayoutQuoteRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -351,7 +348,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WiseRecipientCreateRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -431,7 +423,7 @@ impl .headers(types::PayoutRecipientType::get_headers( self, req, connectors, )?) - .body(types::PayoutRecipientType::get_request_body( + .set_body(types::PayoutRecipientType::get_request_body( self, req, connectors, )?) .build(); @@ -527,14 +519,9 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WisePayoutCreateRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -547,7 +534,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = wise::WisePayoutFulfillRequest::try_from(req)?; - let wise_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(wise_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -648,7 +630,7 @@ impl services::ConnectorIntegration, _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = worldline::ApproveRequest::try_from(req)?; - let worldline_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(worldline_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -407,7 +401,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = worldline::WorldlineRouterData::try_from(( &self.get_currency_unit(), req.request.currency, @@ -501,12 +495,7 @@ impl ConnectorIntegration::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(worldline_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -528,7 +517,7 @@ impl ConnectorIntegration, _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_req = worldline::WorldlineRefundRequest::try_from(req)?; - let refund_req = types::RequestBody::log_and_get_request_body( - &connector_req, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(refund_req)) + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -617,7 +601,7 @@ impl ConnectorIntegration, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let header_value = conn_utils::get_header_key_value("X-GCS-Signature", request.headers)?; + let header_value = + connector_utils::get_header_key_value("X-GCS-Signature", request.headers)?; let signature = consts::BASE64_ENGINE .decode(header_value.as_bytes()) .into_report() diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index bfa58a74f331..6e898f531da4 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -4,7 +4,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{crypto, ext_traits::ByteSliceExt}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use transformers as worldpay; @@ -26,7 +26,7 @@ use crate::{ api::{self, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, - utils::{self as ext_traits, BytesExt}, + utils::BytesExt, }; #[derive(Debug, Clone)] @@ -277,7 +277,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = worldpay::WorldpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let connector_request = WorldpayPaymentsRequest::try_from(&connector_router_data)?; - let worldpay_payment_request = types::RequestBody::log_and_get_request_body( - &connector_request, - ext_traits::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(worldpay_payment_request)) + let connector_req = WorldpayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -470,7 +465,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = WorldpayRefundRequest::try_from(req)?; - let fiserv_refund_request = types::RequestBody::log_and_get_request_body( - &connector_request, - ext_traits::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(fiserv_refund_request)) + ) -> CustomResult { + let connector_req = WorldpayRefundRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn get_url( @@ -560,7 +550,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.amount, req, ))?; - let req_obj = zen::ZenPaymentsRequest::try_from(&connector_router_data)?; - let zen_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(zen_req)) + let connector_req = zen::ZenPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -249,7 +244,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult, errors::ConnectorError> { + ) -> CustomResult { let connector_router_data = zen::ZenRouterData::try_from(( &self.get_currency_unit(), req.request.currency, req.request.refund_amount, req, ))?; - let req_obj = zen::ZenRefundRequest::try_from(&connector_router_data)?; - let zen_req = types::RequestBody::log_and_get_request_body( - &req_obj, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some(zen_req)) + let connector_req = zen::ZenRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -444,7 +434,7 @@ impl ConnectorIntegration { pub card_number: cards::CardNumber, - pub customer_id: &'a str, + pub customer_id: String, pub card_exp_month: Secret, pub card_exp_year: Secret, pub merchant_id: &'a str, @@ -323,7 +323,7 @@ pub async fn mk_add_locker_request_hs<'a>( url.push_str("/cards/add"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(body.to_string()))); Ok(request) } @@ -400,7 +400,7 @@ pub fn mk_add_card_request( card: &api::CardDetail, customer_id: &str, _req: &api::PaymentMethodCreate, - locker_id: &str, + locker_id: &'static str, merchant_id: &str, ) -> CustomResult { let customer_id = if cfg!(feature = "release") { @@ -410,7 +410,7 @@ pub fn mk_add_card_request( }; let add_card_req = AddCardRequest { card_number: card.card_number.clone(), - customer_id: &customer_id, + customer_id, card_exp_month: card.card_exp_month.clone(), card_exp_year: card.card_exp_year.clone(), merchant_id: locker_id, @@ -421,16 +421,10 @@ pub fn mk_add_card_request( name_on_card: Some("John Doe".to_string().into()), // [#256] nickname: Some("router".to_string()), // }; - let body = utils::Encode::>::url_encode(&add_card_req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/addCard"); let mut request = services::Request::new(services::Method::Post, &url); - request.add_header( - headers::CONTENT_TYPE, - "application/x-www-form-urlencoded".into(), - ); - request.set_body(body); + request.set_body(RequestContent::FormUrlEncoded(Box::new(add_card_req))); Ok(request) } @@ -475,30 +469,24 @@ pub async fn mk_get_card_request_hs( url.push_str("/cards/retrieve"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(body.to_string()))); Ok(request) } -pub fn mk_get_card_request<'a>( +pub fn mk_get_card_request( locker: &settings::Locker, - locker_id: &'a str, - card_id: &'a str, + locker_id: &'static str, + card_id: &'static str, ) -> CustomResult { let get_card_req = GetCard { merchant_id: locker_id, card_id, }; - let body = utils::Encode::>::url_encode(&get_card_req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/getCard"); let mut request = services::Request::new(services::Method::Post, &url); - request.add_header( - headers::CONTENT_TYPE, - "application/x-www-form-urlencoded".into(), - ); - request.set_body(body); + request.set_body(RequestContent::FormUrlEncoded(Box::new(get_card_req))); Ok(request) } @@ -555,31 +543,25 @@ pub async fn mk_delete_card_request_hs( url.push_str("/cards/delete"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(body.to_string()))); Ok(request) } -pub fn mk_delete_card_request<'a>( +pub fn mk_delete_card_request( locker: &settings::Locker, - merchant_id: &'a str, - card_id: &'a str, + merchant_id: &'static str, + card_id: &'static str, ) -> CustomResult { let delete_card_req = GetCard { merchant_id, card_id, }; - let body = utils::Encode::>::url_encode(&delete_card_req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/deleteCard"); let mut request = services::Request::new(services::Method::Post, &url); request.add_default_headers(); - request.add_header( - headers::CONTENT_TYPE, - "application/x-www-form-urlencoded".into(), - ); - request.set_body(body); + request.set_body(RequestContent::FormUrlEncoded(Box::new(delete_card_req))); Ok(request) } @@ -627,7 +609,7 @@ pub fn mk_crud_locker_request( let mut request = services::Request::new(services::Method::Post, &url); request.add_default_headers(); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(body.to_string()); + request.set_body(RequestContent::Json(Box::new(body.to_string()))); Ok(request) } diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 595a6f5e958e..91513019179f 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -1,6 +1,6 @@ use api_models::payments as payment_types; use async_trait::async_trait; -use common_utils::ext_traits::ByteSliceExt; +use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, Report, ResultExt}; #[cfg(feature = "kms")] use external_services::kms; @@ -15,7 +15,7 @@ use crate::{ routes::{self, metrics}, services, types::{self, api, domain}, - utils::{self, OptionExt}, + utils::OptionExt, }; #[async_trait] @@ -124,13 +124,6 @@ fn build_apple_pay_session_request( apple_pay_merchant_cert: String, apple_pay_merchant_cert_key: String, ) -> RouterResult { - let applepay_session_request = types::RequestBody::log_and_get_request_body( - &request, - utils::Encode::::encode_to_string_of_json, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode ApplePay session request to a string of json")?; - let mut url = state.conf.connectors.applepay.base_url.to_owned(); url.push_str("paymentservices/paymentSession"); @@ -142,7 +135,7 @@ fn build_apple_pay_session_request( headers::CONTENT_TYPE.to_string(), "application/json".to_string().into(), )]) - .body(Some(applepay_session_request)) + .set_body(RequestContent::Json(Box::new(request))) .add_certificate(Some(apple_pay_merchant_cert)) .add_certificate_key(Some(apple_pay_merchant_cert_key)) .build(); diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index c43c00b7259c..3d150e6eb4c8 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -929,7 +929,9 @@ pub async fn start_refund_workflow( refund_tracker: &storage::ProcessTracker, ) -> Result<(), errors::ProcessTrackerError> { match refund_tracker.name.as_deref() { - Some("EXECUTE_REFUND") => trigger_refund_execute_workflow(state, refund_tracker).await, + Some("EXECUTE_REFUND") => { + Box::pin(trigger_refund_execute_workflow(state, refund_tracker)).await + } Some("SYNC_REFUND") => { Box::pin(sync_refund_with_gateway_workflow(state, refund_tracker)).await } diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index e643e0455b8b..bac47b34dced 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -1,16 +1,11 @@ pub mod utils; use api_models::verifications::{self, ApplepayMerchantResponse}; -use common_utils::{errors::CustomResult, ext_traits::Encode}; +use common_utils::{errors::CustomResult, request::RequestContent}; use error_stack::ResultExt; #[cfg(feature = "kms")] use external_services::kms; -use crate::{ - core::errors::{self, api_error_response}, - headers, logger, - routes::AppState, - services, types, -}; +use crate::{core::errors::api_error_response, headers, logger, routes::AppState, services}; const APPLEPAY_INTERNAL_MERCHANT_NAME: &str = "Applepay_merchant"; @@ -57,13 +52,6 @@ pub async fn verify_merchant_creds_for_applepay( partner_merchant_name: APPLEPAY_INTERNAL_MERCHANT_NAME.to_string(), }; - let applepay_req = types::RequestBody::log_and_get_request_body( - &request_body, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode ApplePay session request to a string of json")?; - let apple_pay_merch_verification_req = services::RequestBuilder::new() .method(services::Method::Post) .url(applepay_endpoint) @@ -72,7 +60,7 @@ pub async fn verify_merchant_creds_for_applepay( headers::CONTENT_TYPE.to_string(), "application/json".to_string().into(), )]) - .body(Some(applepay_req)) + .set_body(RequestContent::Json(Box::new(request_body))) .add_certificate(Some(cert_data)) .add_certificate_key(Some(key_data)) .build(); diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index be8d118a47c2..762ee19b6415 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -8,7 +8,7 @@ use api_models::{ payments::HeaderPayload, webhooks::{self, WebhookResponseTracker}, }; -use common_utils::{errors::ReportSwitchExt, events::ApiEventsType}; +use common_utils::{errors::ReportSwitchExt, events::ApiEventsType, request::RequestContent}; use error_stack::{report, IntoReport, ResultExt}; use masking::ExposeInterface; use router_env::{instrument, tracing, tracing_actix_web::RequestId}; @@ -30,13 +30,12 @@ use crate::{ routes::{app::AppStateInfo, lock_utils, metrics::request::add_attributes, AppState}, services::{self, authentication as auth}, types::{ - self as router_types, api::{self, mandates::MandateResponseExt}, domain, storage::{self, enums}, transformers::{ForeignInto, ForeignTryInto}, }, - utils::{self as helper_utils, generate_id, Encode, OptionExt, ValueExt}, + utils::{self as helper_utils, generate_id, OptionExt, ValueExt}, }; const OUTGOING_WEBHOOK_TIMEOUT_SECS: u64 = 5; @@ -742,7 +741,7 @@ pub async fn create_event_and_trigger_outgoing_webhook(business_profile, outgoing_webhook, &state).await; + trigger_webhook_to_merchant::(business_profile, outgoing_webhook, state).await; if let Err(e) = result { logger::error!(?e); @@ -756,7 +755,7 @@ pub async fn create_event_and_trigger_outgoing_webhook( business_profile: diesel_models::business_profile::BusinessProfile, webhook: api::OutgoingWebhook, - state: &AppState, + state: AppState, ) -> CustomResult<(), errors::WebhooksFlowError> { let webhook_details_json = business_profile .webhook_details @@ -781,13 +780,6 @@ pub async fn trigger_webhook_to_merchant( let outgoing_webhooks_signature = transformed_outgoing_webhook .get_outgoing_webhooks_signature(business_profile.payment_response_hash_key.clone())?; - let transformed_outgoing_webhook_string = router_types::RequestBody::log_and_get_request_body( - &transformed_outgoing_webhook, - Encode::::encode_to_string_of_json, - ) - .change_context(errors::WebhooksFlowError::OutgoingWebhookEncodingFailed) - .attach_printable("There was an issue when encoding the outgoing webhook body")?; - let mut header = vec![( reqwest::header::CONTENT_TYPE.to_string(), "application/json".into(), @@ -802,12 +794,12 @@ pub async fn trigger_webhook_to_merchant( .url(&webhook_url) .attach_default_headers() .headers(header) - .body(Some(transformed_outgoing_webhook_string)) + .set_body(RequestContent::Json(Box::new(transformed_outgoing_webhook))) .build(); let response = state .api_client - .send_request(state, request, Some(OUTGOING_WEBHOOK_TIMEOUT_SECS), false) + .send_request(&state, request, Some(OUTGOING_WEBHOOK_TIMEOUT_SECS), false) .await; metrics::WEBHOOK_OUTGOING_COUNT.add( @@ -1134,9 +1126,7 @@ pub async fn webhooks_core + Sync + Send + std::fmt::Debug + Serialize + From + Sync + Send + std::fmt::Debug + 'static { fn get_outgoing_webhooks_signature( &self, diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 8f980fee504a..2dc9258e19df 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -6,6 +6,7 @@ use storage_impl::errors::ApplicationError; use crate::{db::KafkaProducer, services::kafka::KafkaSettings}; pub mod api_logs; +pub mod connector_api_logs; pub mod event_logger; pub mod kafka_handler; @@ -29,6 +30,7 @@ pub enum EventType { PaymentAttempt, Refund, ApiLogs, + ConnectorApiLogs, } #[derive(Debug, Default, Deserialize, Clone)] diff --git a/crates/router/src/events/connector_api_logs.rs b/crates/router/src/events/connector_api_logs.rs new file mode 100644 index 000000000000..871a7af0d772 --- /dev/null +++ b/crates/router/src/events/connector_api_logs.rs @@ -0,0 +1,69 @@ +use common_utils::request::Method; +use router_env::tracing_actix_web::RequestId; +use serde::Serialize; +use time::OffsetDateTime; + +use super::{EventType, RawEvent}; + +#[derive(Debug, Serialize)] +pub struct ConnectorEvent { + connector_name: String, + flow: String, + request: String, + response: Option, + url: String, + method: String, + payment_id: String, + merchant_id: String, + created_at: i128, + request_id: String, + latency: u128, +} + +impl ConnectorEvent { + #[allow(clippy::too_many_arguments)] + pub fn new( + connector_name: String, + flow: &str, + request: serde_json::Value, + response: Option, + url: String, + method: Method, + payment_id: String, + merchant_id: String, + request_id: Option<&RequestId>, + latency: u128, + ) -> Self { + Self { + connector_name, + flow: flow + .rsplit_once("::") + .map(|(_, s)| s) + .unwrap_or(flow) + .to_string(), + request: request.to_string(), + response, + url, + method: method.to_string(), + payment_id, + merchant_id, + created_at: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, + request_id: request_id + .map(|i| i.as_hyphenated().to_string()) + .unwrap_or("NO_REQUEST_ID".to_string()), + latency, + } + } +} + +impl TryFrom for RawEvent { + type Error = serde_json::Error; + + fn try_from(value: ConnectorEvent) -> Result { + Ok(Self { + event_type: EventType::ConnectorApiLogs, + key: value.request_id.clone(), + payload: serde_json::to_value(value)?, + }) + } +} diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index ce6a2a97e28d..ab404125a384 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -360,7 +360,7 @@ pub async fn payment_connector_update( let flow = Flow::MerchantConnectorsUpdate; let (merchant_id, merchant_connector_id) = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -375,7 +375,7 @@ pub async fn payment_connector_update( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Connector - Delete diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index fdfd3fc21147..0357cedd443c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -61,6 +61,7 @@ pub struct AppState { pub api_client: Box, #[cfg(feature = "olap")] pub pool: crate::analytics::AnalyticsProvider, + pub request_id: Option, } impl scheduler::SchedulerAppState for AppState { @@ -97,7 +98,8 @@ impl AppStateInfo for AppState { } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); - self.store.add_request_id(request_id.to_string()) + self.store.add_request_id(request_id.to_string()); + self.request_id.replace(request_id); } fn add_merchant_id(&mut self, merchant_id: Option) { @@ -226,6 +228,7 @@ impl AppState { event_handler, #[cfg(feature = "olap")] pool, + request_id: None, } }) .await diff --git a/crates/router/src/routes/locker_migration.rs b/crates/router/src/routes/locker_migration.rs index 892dc5941bd6..a3df0c3a229b 100644 --- a/crates/router/src/routes/locker_migration.rs +++ b/crates/router/src/routes/locker_migration.rs @@ -14,7 +14,7 @@ pub async fn rust_locker_migration( ) -> HttpResponse { let flow = Flow::RustLockerMigration; let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -22,6 +22,6 @@ pub async fn rust_locker_migration( |state, _, _| locker_migration::rust_locker_migration(state, &merchant_id), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 918aab929ac9..7680ff29d45c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -16,9 +16,10 @@ pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; use common_utils::{ consts::X_HS_LATENCY, errors::{ErrorSwitch, ReportSwitchExt}, + request::RequestContent, }; use error_stack::{report, IntoReport, Report, ResultExt}; -use masking::{ExposeOptionInterface, PeekInterface}; +use masking::PeekInterface; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; use serde_json::json; @@ -34,7 +35,10 @@ use crate::{ errors::{self, CustomResult}, payments, }, - events::api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, + events::{ + api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, + connector_api_logs::ConnectorEvent, + }, logger, routes::{ app::AppStateInfo, @@ -133,8 +137,8 @@ pub trait ConnectorIntegration: ConnectorIntegrationAny, _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(None) + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(json!(r#"{}"#)))) } fn get_request_form_data( @@ -278,6 +282,16 @@ where { // If needed add an error stack as follows // connector_integration.build_request(req).attach_printable("Failed to build request"); + logger::debug!(connector_request=?connector_request); + let masked_conn_req = connector_request.as_ref().map(|req| match &req.body { + Some(RequestContent::Json(payload)) + | Some(RequestContent::FormUrlEncoded(payload)) + | Some(RequestContent::Xml(payload)) => payload.masked_serialize().unwrap_or_default(), + Some(RequestContent::FormData(_)) => json!({"request_type": "FORM_DATA"}), + None => serde_json::Value::Null, + }); + logger::debug!(connector_request_body=?masked_conn_req); + logger::debug!(payment_id=?req.payment_id); let mut router_data = req.clone(); match call_connector_action { payments::CallConnectorAction::HandleResponse(res) => { @@ -351,10 +365,48 @@ where match connector_request { Some(request) => { logger::debug!(connector_request=?request); + + let masked_request_body = match &request.body { + Some(request) => match request { + RequestContent::Json(i) + | RequestContent::FormUrlEncoded(i) + | RequestContent::Xml(i) => i + .masked_serialize() + .unwrap_or(json!({ "error": "failed to mask serialize"})), + RequestContent::FormData(_) => json!({"request_type": "FORM_DATA"}), + }, + None => serde_json::Value::Null, + }; + let request_url = request.url.clone(); + let request_method = request.method; + let current_time = Instant::now(); let response = call_connector_api(state, request).await; let external_latency = current_time.elapsed().as_millis(); logger::debug!(connector_response=?response); + + let connector_event = ConnectorEvent::new( + req.connector.clone(), + std::any::type_name::(), + masked_request_body, + None, + request_url, + request_method, + req.payment_id.clone(), + req.merchant_id.clone(), + state.request_id.as_ref(), + external_latency, + ); + + match connector_event.try_into() { + Ok(event) => { + state.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, "Error Logging Connector Event"); + } + } + match response { Ok(body) => { let response = match body { @@ -477,7 +529,7 @@ pub async fn send_request( request: Request, option_timeout_secs: Option, ) -> CustomResult { - logger::debug!(method=?request.method, headers=?request.headers, payload=?request.payload, ?request); + logger::debug!(method=?request.method, headers=?request.headers, payload=?request.body, ?request); let url = reqwest::Url::parse(&request.url) .into_report() @@ -508,46 +560,49 @@ pub async fn send_request( Method::Get => client.get(url), Method::Post => { let client = client.post(url); - match request.content_type { - Some(ContentType::Json) => client.json(&request.payload), - - Some(ContentType::FormData) => { - client.multipart(request.form_data.unwrap_or_default()) + match request.body { + Some(RequestContent::Json(payload)) => client.json(&payload), + Some(RequestContent::FormData(form)) => client.multipart(form), + Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), + Some(RequestContent::Xml(payload)) => { + let body = quick_xml::se::to_string(&payload) + .into_report() + .change_context(errors::ApiClientError::BodySerializationFailed)?; + client.body(body).header("Content-Type", "application/xml") } - - // Currently this is not used remove this if not required - // If using this then handle the serde_part - Some(ContentType::FormUrlEncoded) => { - let payload = match request.payload.clone() { - Some(req) => serde_json::from_str(req.peek()) - .into_report() - .change_context(errors::ApiClientError::UrlEncodingFailed)?, - _ => json!(r#""#), - }; - let url_encoded_payload = serde_urlencoded::to_string(&payload) + None => client, + } + } + Method::Put => { + let client = client.put(url); + match request.body { + Some(RequestContent::Json(payload)) => client.json(&payload), + Some(RequestContent::FormData(form)) => client.multipart(form), + Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), + Some(RequestContent::Xml(payload)) => { + let body = quick_xml::se::to_string(&payload) .into_report() - .change_context(errors::ApiClientError::UrlEncodingFailed) - .attach_printable_lazy(|| { - format!( - "Unable to do url encoding on request: {:?}", - &request.payload - ) - })?; - - logger::debug!(?url_encoded_payload); - client.body(url_encoded_payload) + .change_context(errors::ApiClientError::BodySerializationFailed)?; + client.body(body).header("Content-Type", "application/xml") } - // If payload needs processing the body cannot have default - None => client.body(request.payload.expose_option().unwrap_or_default()), + None => client, + } + } + Method::Patch => { + let client = client.patch(url); + match request.body { + Some(RequestContent::Json(payload)) => client.json(&payload), + Some(RequestContent::FormData(form)) => client.multipart(form), + Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), + Some(RequestContent::Xml(payload)) => { + let body = quick_xml::se::to_string(&payload) + .into_report() + .change_context(errors::ApiClientError::BodySerializationFailed)?; + client.body(body).header("Content-Type", "application/xml") + } + None => client, } } - - Method::Put => client - .put(url) - .body(request.payload.expose_option().unwrap_or_default()), // If payload needs processing the body cannot have default - Method::Patch => client - .patch(url) - .body(request.payload.expose_option().unwrap_or_default()), Method::Delete => client.delete(url), } .add_headers(headers) diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs index 497ac16721b5..4c65b4677872 100644 --- a/crates/router/src/services/kafka.rs +++ b/crates/router/src/services/kafka.rs @@ -83,6 +83,7 @@ pub struct KafkaSettings { attempt_analytics_topic: String, refund_analytics_topic: String, api_logs_topic: String, + connector_logs_topic: String, } impl KafkaSettings { @@ -119,7 +120,15 @@ impl KafkaSettings { Err(ApplicationError::InvalidConfigurationValueError( "Kafka API event Analytics topic must not be empty".into(), )) - }) + })?; + + common_utils::fp_utils::when(self.connector_logs_topic.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "Kafka Connector Logs topic must not be empty".into(), + )) + })?; + + Ok(()) } } @@ -130,6 +139,7 @@ pub struct KafkaProducer { attempt_analytics_topic: String, refund_analytics_topic: String, api_logs_topic: String, + connector_logs_topic: String, } struct RdKafkaProducer(ThreadedProducer); @@ -166,6 +176,7 @@ impl KafkaProducer { attempt_analytics_topic: conf.attempt_analytics_topic.clone(), refund_analytics_topic: conf.refund_analytics_topic.clone(), api_logs_topic: conf.api_logs_topic.clone(), + connector_logs_topic: conf.connector_logs_topic.clone(), }) } @@ -297,6 +308,7 @@ impl KafkaProducer { EventType::PaymentAttempt => &self.attempt_analytics_topic, EventType::PaymentIntent => &self.intent_analytics_topic, EventType::Refund => &self.refund_analytics_topic, + EventType::ConnectorApiLogs => &self.connector_logs_topic, } } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index f50a936aac8b..ecbea1e793c6 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -21,7 +21,7 @@ pub use api_models::{ enums::{Connector, PayoutConnectors}, payouts as payout_types, }; -pub use common_utils::request::RequestBody; +pub use common_utils::request::{RequestBody, RequestContent}; use common_utils::{pii, pii::Email}; use data_models::mandates::MandateData; use error_stack::{IntoReport, ResultExt}; diff --git a/crates/router/src/utils/connector_onboarding/paypal.rs b/crates/router/src/utils/connector_onboarding/paypal.rs index c803775be071..6d7f2692be72 100644 --- a/crates/router/src/utils/connector_onboarding/paypal.rs +++ b/crates/router/src/utils/connector_onboarding/paypal.rs @@ -1,10 +1,6 @@ -use common_utils::{ - ext_traits::Encode, - request::{Method, Request, RequestBuilder}, -}; +use common_utils::request::{Method, Request, RequestBuilder, RequestContent}; use error_stack::{IntoReport, ResultExt}; use http::header; -use serde_json::json; use crate::{ connector, @@ -51,15 +47,8 @@ pub fn build_paypal_post_request( access_token: String, ) -> RouterResult where - T: serde::Serialize, + T: serde::Serialize + Send + 'static, { - let body = types::RequestBody::log_and_get_request_body( - &json!(body), - Encode::::encode_to_string_of_json, - ) - .change_context(ApiErrorResponse::InternalServerError) - .attach_printable("Failed to build request body")?; - Ok(RequestBuilder::new() .method(Method::Post) .url(&url) @@ -72,7 +61,7 @@ where header::CONTENT_TYPE.to_string().as_str(), "application/json", ) - .body(Some(body)) + .set_body(RequestContent::Json(Box::new(body))) .build()) } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js index ac3f862e43f4..e7f91410f313 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" because payment gets succeeded after one day. if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'processing'", + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index 21f054843897..e37391b78b5c 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js index a6976d95f69e..81ce7784e832 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id - Content check if value for 'status' matches 'processing'", + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js index b160ad9dc04b..9b9d104698f4 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -86,9 +86,9 @@ if (jsonData?.amount) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index b1d5ad5ebbf8..c2a8a615d1c2 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "automatic", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js index f87069589f0a..80343f371a60 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments:id - Content check if value for 'status' matches 'processing'", + "[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js index 255743af78c7..91c871e2160c 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js @@ -65,9 +65,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'", + "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js index 4fbefdb8494a..ef09f8071d62 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.test.js @@ -63,9 +63,9 @@ if (jsonData?.client_secret) { // Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments:id - Content check if value for 'status' matches 'processing'", + "[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("succeeded"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js index fa6deebe16a8..ea70c2aace0e 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Capture/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "processing" for "status" +// Response body should have value "partially_captured" for "status" if (jsonData?.status) { pm.test( - "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'", + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json index 5e3ff0e70ad2..878ea581a938 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Create/request.json @@ -25,7 +25,7 @@ "business_label": "default", "capture_method": "manual", "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1, + "amount_to_capture": 6540, "customer_id": "bernard123", "email": "guest@example.com", "name": "John Doe", diff --git a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js index b1b53a360e32..cc4a60507a89 100644 --- a/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js +++ b/postman/collection-dir/bankofamerica/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture/Payments - Retrieve/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "processing" for "status" +// Response body should have value "partially_captured" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'processing'", + "[POST]::/payments - Content check if value for 'status' matches 'partially_captured'", function () { - pm.expect(jsonData.status).to.eql("processing"); + pm.expect(jsonData.status).to.eql("partially_captured"); }, ); } diff --git a/postman/collection-json/bankofamerica.postman_collection.json b/postman/collection-json/bankofamerica.postman_collection.json index 01524d91953d..cfef0bd7a2f0 100644 --- a/postman/collection-json/bankofamerica.postman_collection.json +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -2391,9 +2391,9 @@ "// Response body should have value \"processing\" for \"status\" because payment gets succeeded after one day.", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -2518,9 +2518,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -2792,9 +2792,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -2957,9 +2957,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3221,9 +3221,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3377,9 +3377,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3622,9 +3622,9 @@ "// Response body should have value \"processing\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", "}", @@ -3766,12 +3766,12 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"partially_captured\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", From bca7cdb4c14b5fbb40d8cbf59fd1756ad27ac674 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Wed, 13 Dec 2023 12:28:40 +0530 Subject: [PATCH 191/443] feat(external_services): adds encrypt function for KMS (#3111) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/external_services/src/kms.rs | 97 ++++++++++++++++++++++++++++- crates/external_services/src/lib.rs | 6 +- crates/router/src/routes/metrics.rs | 4 +- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/crates/external_services/src/kms.rs b/crates/external_services/src/kms.rs index 31c82253fe80..04a58e4b23f4 100644 --- a/crates/external_services/src/kms.rs +++ b/crates/external_services/src/kms.rs @@ -75,7 +75,7 @@ impl KmsClient { // Logging using `Debug` representation of the error as the `Display` // representation does not hold sufficient information. logger::error!(kms_sdk_error=?error, "Failed to KMS decrypt data"); - metrics::AWS_KMS_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::AWS_KMS_DECRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); error }) .into_report() @@ -96,11 +96,51 @@ impl KmsClient { Ok(output) } + + /// Encrypts the provided String data using the AWS KMS SDK. We assume that + /// the SDK has the values required to interact with the AWS KMS APIs (`AWS_ACCESS_KEY_ID` and + /// `AWS_SECRET_ACCESS_KEY`) either set in environment variables, or that the SDK is running in + /// a machine that is able to assume an IAM role. + pub async fn encrypt(&self, data: impl AsRef<[u8]>) -> CustomResult { + let start = Instant::now(); + let plaintext_blob = Blob::new(data.as_ref()); + + let encrypted_output = self + .inner_client + .encrypt() + .key_id(&self.key_id) + .plaintext(plaintext_blob) + .send() + .await + .map_err(|error| { + // Logging using `Debug` representation of the error as the `Display` + // representation does not hold sufficient information. + logger::error!(kms_sdk_error=?error, "Failed to KMS encrypt data"); + metrics::AWS_KMS_ENCRYPTION_FAILURES.add(&metrics::CONTEXT, 1, &[]); + error + }) + .into_report() + .change_context(KmsError::EncryptionFailed)?; + + let output = encrypted_output + .ciphertext_blob + .ok_or(KmsError::MissingCiphertextEncryptionOutput) + .into_report() + .map(|blob| consts::BASE64_ENGINE.encode(blob.into_inner()))?; + let time_taken = start.elapsed(); + metrics::AWS_KMS_ENCRYPT_TIME.record(&metrics::CONTEXT, time_taken.as_secs_f64(), &[]); + + Ok(output) + } } /// Errors that could occur during KMS operations. #[derive(Debug, thiserror::Error)] pub enum KmsError { + /// An error occurred when base64 encoding input data. + #[error("Failed to base64 encode input data")] + Base64EncodingFailed, + /// An error occurred when base64 decoding input data. #[error("Failed to base64 decode input data")] Base64DecodingFailed, @@ -109,10 +149,18 @@ pub enum KmsError { #[error("Failed to KMS decrypt input data")] DecryptionFailed, + /// An error occurred when KMS encrypting input data. + #[error("Failed to KMS encrypt input data")] + EncryptionFailed, + /// The KMS decrypted output does not include a plaintext output. #[error("Missing plaintext KMS decryption output")] MissingPlaintextDecryptionOutput, + /// The KMS encrypted output does not include a ciphertext output. + #[error("Missing ciphertext KMS encryption output")] + MissingCiphertextEncryptionOutput, + /// An error occurred UTF-8 decoding KMS decrypted output. #[error("Failed to UTF-8 decode decryption output")] Utf8DecodingFailed, @@ -147,3 +195,50 @@ impl common_utils::ext_traits::ConfigExt for KmsValue { self.0.peek().is_empty_after_trim() } } + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + #[tokio::test] + async fn check_kms_encryption() { + std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); + std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); + use super::*; + let config = KmsConfig { + key_id: "YOUR KMS KEY ID".to_string(), + region: "AWS REGION".to_string(), + }; + + let data = "hello".to_string(); + let binding = data.as_bytes(); + let kms_encrypted_fingerprint = KmsClient::new(&config) + .await + .encrypt(binding) + .await + .expect("kms encryption failed"); + + println!("{}", kms_encrypted_fingerprint); + } + + #[tokio::test] + async fn check_kms_decrypt() { + std::env::set_var("AWS_SECRET_ACCESS_KEY", "YOUR SECRET ACCESS KEY"); + std::env::set_var("AWS_ACCESS_KEY_ID", "YOUR AWS ACCESS KEY ID"); + use super::*; + let config = KmsConfig { + key_id: "YOUR KMS KEY ID".to_string(), + region: "AWS REGION".to_string(), + }; + + // Should decrypt to hello + let data = "KMS ENCRYPTED CIPHER".to_string(); + let binding = data.as_bytes(); + let kms_encrypted_fingerprint = KmsClient::new(&config) + .await + .decrypt(binding) + .await + .expect("kms decryption failed"); + + println!("{}", kms_encrypted_fingerprint); + } +} diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index fa57a0bac9c6..ccf1db47a3ae 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -26,8 +26,12 @@ pub mod metrics { global_meter!(GLOBAL_METER, "EXTERNAL_SERVICES"); #[cfg(feature = "kms")] - counter_metric!(AWS_KMS_FAILURES, GLOBAL_METER); // No. of AWS KMS API failures + counter_metric!(AWS_KMS_DECRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Decryption failures + #[cfg(feature = "kms")] + counter_metric!(AWS_KMS_ENCRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Encryption failures #[cfg(feature = "kms")] histogram_metric!(AWS_KMS_DECRYPT_TIME, GLOBAL_METER); // Histogram for KMS decryption time (in sec) + #[cfg(feature = "kms")] + histogram_metric!(AWS_KMS_ENCRYPT_TIME, GLOBAL_METER); // Histogram for KMS encryption time (in sec) } diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index 192df1a09298..b3629ab7d52b 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -6,7 +6,9 @@ global_meter!(GLOBAL_METER, "ROUTER_API"); counter_metric!(HEALTH_METRIC, GLOBAL_METER); // No. of health API hits counter_metric!(KV_MISS, GLOBAL_METER); // No. of KV misses #[cfg(feature = "kms")] -counter_metric!(AWS_KMS_FAILURES, GLOBAL_METER); // No. of AWS KMS API failures +counter_metric!(AWS_KMS_ENCRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Encryption failures +#[cfg(feature = "kms")] +counter_metric!(AWS_KMS_DECRYPTION_FAILURES, GLOBAL_METER); // No. of AWS KMS Decryption failures // API Level Metrics counter_metric!(REQUESTS_RECEIVED, GLOBAL_METER); From 3cc9642f3ac4c07fb675e9ff4032832819d877a1 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:54:08 +0530 Subject: [PATCH 192/443] feat(connector): [HELCIM] Add connector_request_reference_id in invoice_number (#3087) --- config/config.example.toml | 4 + config/development.toml | 4 + config/docker_compose.toml | 4 + crates/router/src/configs/defaults.rs | 166 ++++++++++++++++++ .../src/connector/helcim/transformers.rs | 68 ++++++- 5 files changed, 237 insertions(+), 9 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 8618a4031f9f..fb1c12a7d7ef 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -446,6 +446,10 @@ ach = { currency = "USD" } [pm_filters.prophetpay] card_redirect = { currency = "USD" } +[pm_filters.helcim] +credit = { currency = "USD" } +debit = { currency = "USD" } + [connector_customer] connector_list = "gocardless,stax,stripe" payout_connector_list = "wise" diff --git a/config/development.toml b/config/development.toml index 161914361039..9646a0a0456d 100644 --- a/config/development.toml +++ b/config/development.toml @@ -358,6 +358,10 @@ paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,N credit = { not_available_flows = { capture_method = "manual" } } debit = { not_available_flows = { capture_method = "manual" } } +[pm_filters.helcim] +credit = { currency = "USD" } +debit = { currency = "USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } credit = { not_available_flows = { capture_method = "manual" } } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c89bec1999dc..13d405131bda 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -314,6 +314,10 @@ cashapp = {country = "US", currency = "USD"} [pm_filters.prophetpay] card_redirect = { currency = "USD" } +[pm_filters.helcim] +credit = { currency = "USD" } +debit = { currency = "USD" } + [pm_filters.stax] credit = { currency = "USD" } debit = { currency = "USD" } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 744d7883e950..4abb878043fd 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -1101,6 +1101,89 @@ impl Default for super::settings::RequiredFields { ]), } ), + ( + enums::Connector::Helcim, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Iatapay, RequiredFieldFinal { @@ -3049,6 +3132,89 @@ impl Default for super::settings::RequiredFields { ]), } ), + ( + enums::Connector::Helcim, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Iatapay, RequiredFieldFinal { diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index dc38b2eeb253..a27a562ddc27 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -44,6 +44,19 @@ impl } } +pub fn check_currency( + currency: types::storage::enums::Currency, +) -> Result { + if currency == types::storage::enums::Currency::USD { + Ok(currency) + } else { + Err(errors::ConnectorError::NotSupported { + message: format!("currency {currency} is not supported for this merchant account"), + connector: "Helcim", + })? + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HelcimVerifyRequest { @@ -62,6 +75,7 @@ pub struct HelcimPaymentsRequest { currency: enums::Currency, ip_address: Secret, card_data: HelcimCard, + invoice: HelcimInvoice, billing_address: HelcimBillingAddress, //The ecommerce field is an optional field in Connector Helcim. //Setting the ecommerce field to true activates the Helcim Fraud Defender. @@ -83,6 +97,22 @@ pub struct HelcimBillingAddress { email: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HelcimInvoice { + invoice_number: String, + line_items: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HelcimLineItems { + description: String, + quantity: u8, + price: f64, + total: f64, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct HelcimCard { @@ -111,9 +141,9 @@ impl TryFrom<(&types::SetupMandateRouterData, &api::Card)> for HelcimVerifyReque email: item.request.email.clone(), }; let ip_address = item.request.get_browser_info()?.get_ip_address()?; - + let currency = check_currency(item.request.currency)?; Ok(Self { - currency: item.request.currency, + currency, ip_address, card_data, billing_address, @@ -192,11 +222,30 @@ impl .request .get_browser_info()? .get_ip_address()?; + let line_items = vec![ + (HelcimLineItems { + description: item + .router_data + .description + .clone() + .unwrap_or("No Description".to_string()), + // By default quantity is set to 1 and price and total is set to amount because these three fields are required to generate an invoice. + quantity: 1, + price: item.amount, + total: item.amount, + }), + ]; + let invoice = HelcimInvoice { + invoice_number: item.router_data.connector_request_reference_id.clone(), + line_items, + }; + let currency = check_currency(item.router_data.request.currency)?; Ok(Self { - amount: item.amount.to_owned(), - currency: item.router_data.request.currency, + amount: item.amount, + currency, ip_address, card_data, + invoice, billing_address, ecommerce: None, }) @@ -295,6 +344,7 @@ impl From for enums::AttemptStatus { pub struct HelcimPaymentsResponse { status: HelcimPaymentStatus, transaction_id: u64, + invoice_number: Option, #[serde(rename = "type")] transaction_type: HelcimTransactionType, } @@ -327,7 +377,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), @@ -382,7 +432,7 @@ impl mandate_reference: None, connector_metadata, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), @@ -441,7 +491,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), @@ -528,7 +578,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), @@ -591,7 +641,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: item.response.invoice_number.clone(), incremental_authorization_allowed: None, }), status: enums::AttemptStatus::from(item.response), From d28c82c0ee18125128b3ec17615a87cf930ae5f4 Mon Sep 17 00:00:00 2001 From: Shanks Date: Wed, 13 Dec 2023 14:37:25 +0530 Subject: [PATCH 193/443] ci: add backwards_compatibility flag to feature list (#3124) --- crates/router/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index bc11d921ff4c..a1ddc37bbf2c 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -16,7 +16,7 @@ email = ["external_services/email", "dep:aws-config", "olap"] frm = [] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing"] +release = ["kms", "stripe", "basilisk", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] From be13d15d3c0214c863e131cf1dbe184d5baec5d7 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:22:36 +0530 Subject: [PATCH 194/443] fix: validate refund amount with amount_captured instead of amount (#3120) --- .../router/src/compatibility/stripe/errors.rs | 2 +- .../src/core/errors/api_error_response.rs | 2 +- crates/router/src/core/errors/transformers.rs | 2 +- crates/router/src/core/refunds.rs | 6 +- crates/router/src/core/refunds/validator.rs | 6 +- .../src/routes/dummy_connector/errors.rs | 2 +- crates/router/tests/integration_demo.rs | 2 +- .../Refunds - Create/event.test.js | 4 +- .../event.test.js | 4 +- .../Refunds - Create/event.test.js | 6 +- .../.meta.json | 8 ++ .../Payments - Capture/.event.meta.json | 3 + .../Payments - Capture/event.test.js | 94 +++++++++++++++++++ .../Payments - Capture/request.json | 39 ++++++++ .../Payments - Capture/response.json | 1 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 71 ++++++++++++++ .../Payments - Create/request.json | 87 +++++++++++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 ++++++++++++++ .../Payments - Retrieve/request.json | 28 ++++++ .../Payments - Retrieve/response.json | 1 + .../Refunds - Create/.event.meta.json | 3 + .../Refunds - Create/event.test.js | 58 ++++++++++++ .../Refunds - Create/request.json | 38 ++++++++ .../Refunds - Create/response.json | 1 + .../adyen_uk.postman_collection.json | 4 +- .../bluesnap.postman_collection.json | 4 +- .../checkout.postman_collection.json | 6 +- postman/portman-config.json | 2 +- 31 files changed, 538 insertions(+), 24 deletions(-) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 03b59c55036f..cf49b1aad208 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -33,7 +33,7 @@ pub enum StripeErrorCode { expected_format: String, }, - #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_06", message = "Refund amount exceeds the payment amount.")] + #[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_06", message = "The refund amount exceeds the amount captured.")] RefundAmountExceedsPaymentAmount { param: String }, #[error(error_type = StripeErrorType::ApiError, code = "payment_intent_authentication_failure", message = "Payment failed while processing with connector. Retry payment.")] diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index d34cbf88aaae..ac51c5018df1 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -60,7 +60,7 @@ pub enum ApiErrorResponse { CustomerRedacted, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_12", message = "Reached maximum refund attempts")] MaximumRefundCount, - #[error(error_type = ErrorType::InvalidRequestError, code = "IR_13", message = "Refund amount exceeds the payment amount")] + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_13", message = "The refund amount exceeds the amount captured")] RefundAmountExceedsPaymentAmount, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_14", message = "This Payment could not be {current_flow} because it has a {field_name} of {current_value}. The expected state is {states}")] PaymentUnexpectedState { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 17aa6f3a207a..58eb0213cb64 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -65,7 +65,7 @@ impl ErrorSwitch for ApiErrorRespon } Self::MaximumRefundCount => AER::BadRequest(ApiError::new("IR", 12, "Reached maximum refund attempts", None)), Self::RefundAmountExceedsPaymentAmount => { - AER::BadRequest(ApiError::new("IR", 13, "Refund amount exceeds the payment amount", None)) + AER::BadRequest(ApiError::new("IR", 13, "The refund amount exceeds the amount captured", None)) } Self::PaymentUnexpectedState { current_flow, diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 3d150e6eb4c8..6cc118b0f3c7 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -610,7 +610,11 @@ pub async fn validate_and_create_refund( ), })?; - validator::validate_refund_amount(payment_attempt.amount, &all_refunds, refund_amount) + let total_amount_captured = payment_intent + .amount_captured + .unwrap_or(payment_attempt.amount); + + validator::validate_refund_amount(total_amount_captured, &all_refunds, refund_amount) .change_context(errors::ApiErrorResponse::RefundAmountExceedsPaymentAmount)?; validator::validate_maximum_refund_against_payment_attempt( diff --git a/crates/router/src/core/refunds/validator.rs b/crates/router/src/core/refunds/validator.rs index 6198a6f79a68..cae8f0494bee 100644 --- a/crates/router/src/core/refunds/validator.rs +++ b/crates/router/src/core/refunds/validator.rs @@ -17,7 +17,7 @@ pub const DEFAULT_LIMIT: i64 = 10; pub enum RefundValidationError { #[error("The payment attempt was not successful")] UnsuccessfulPaymentAttempt, - #[error("The refund amount exceeds the payment amount")] + #[error("The refund amount exceeds the amount captured")] RefundAmountExceedsPaymentAmount, #[error("The order has expired")] OrderExpired, @@ -40,7 +40,7 @@ pub fn validate_success_transaction( #[instrument(skip_all)] pub fn validate_refund_amount( - payment_attempt_amount: i64, // &storage::PaymentAttempt, + amount_captured: i64, all_refunds: &[storage::Refund], refund_amount: i64, ) -> CustomResult<(), RefundValidationError> { @@ -58,7 +58,7 @@ pub fn validate_refund_amount( .sum(); utils::when( - refund_amount > (payment_attempt_amount - total_refunded_amount), + refund_amount > (amount_captured - total_refunded_amount), || { Err(report!( RefundValidationError::RefundAmountExceedsPaymentAmount diff --git a/crates/router/src/routes/dummy_connector/errors.rs b/crates/router/src/routes/dummy_connector/errors.rs index 4501df0a0fa4..0b4affa8b619 100644 --- a/crates/router/src/routes/dummy_connector/errors.rs +++ b/crates/router/src/routes/dummy_connector/errors.rs @@ -20,7 +20,7 @@ pub enum DummyConnectorErrors { #[error(error_type = ErrorType::InvalidRequestError, code = "DC_02", message = "Missing required param: {field_name}")] MissingRequiredField { field_name: &'static str }, - #[error(error_type = ErrorType::InvalidRequestError, code = "DC_03", message = "Refund amount exceeds the payment amount")] + #[error(error_type = ErrorType::InvalidRequestError, code = "DC_03", message = "The refund amount exceeds the amount captured")] RefundAmountExceedsPaymentAmount, #[error(error_type = ErrorType::InvalidRequestError, code = "DC_04", message = "Card not supported. Please use test cards")] diff --git a/crates/router/tests/integration_demo.rs b/crates/router/tests/integration_demo.rs index 5bdf9a5f525e..5d2c4a7943b4 100644 --- a/crates/router/tests/integration_demo.rs +++ b/crates/router/tests/integration_demo.rs @@ -154,6 +154,6 @@ async fn exceed_refund() { let message: serde_json::Value = user_client.create_refund(&server, &payment_id, 100).await; assert_eq!( message["error"]["message"], - "Refund amount exceeds the payment amount." + "The refund amount exceeds the amount captured." ); } diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js index a195cd43879e..24327b0aad03 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario7-Refund exceeds amount/Refunds - Create/event.test.js @@ -50,10 +50,10 @@ if (jsonData?.error?.type) { // Response body should have value "invalid_request" for "error type" if (jsonData?.error?.message) { pm.test( - "[POST]::/payments - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'", + "[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'", function () { pm.expect(jsonData.error.message).to.eql( - "Refund amount exceeds the payment amount", + "The refund amount exceeds the amount captured", ); }, ); diff --git a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js index 71324af2c819..2f5de7240613 100644 --- a/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js +++ b/postman/collection-dir/bluesnap/Flow Testcases/Happy Cases/Scenario28-Create partially captured payment with refund/Refunds - Validation should throw/event.test.js @@ -32,9 +32,9 @@ if (jsonData?.refund_id) { // Response body should have value "succeeded" for "status" if (jsonData?.error.message) { pm.test( - "[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'", + "[POST]::/refunds - Content check if value for 'message' matches 'The refund amount exceeds the amount captured'", function () { - pm.expect(jsonData.error.message).to.eql("Refund amount exceeds the payment amount"); + pm.expect(jsonData.error.message).to.eql("The refund amount exceeds the amount captured"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js index 00f93b844c5b..d5f63c170ffe 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/event.test.js @@ -47,13 +47,13 @@ if (jsonData?.error?.type) { ); } -// Response body should have value "Refund amount exceeds the payment amount" for "message" +// Response body should have value "The refund amount exceeds the amount captured" for "message" if (jsonData?.error?.message) { pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'", + "[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'", function () { pm.expect(jsonData.error.message).to.eql( - "Refund amount exceeds the payment amount", + "The refund amount exceeds the amount captured", ); }, ); diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json new file mode 100644 index 000000000000..7dd4c0a0c214 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/.meta.json @@ -0,0 +1,8 @@ +{ + "childrenOrder": [ + "Payments - Create", + "Payments - Capture", + "Payments - Retrieve", + "Refunds - Create" + ] +} \ No newline at end of file diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/event.test.js new file mode 100644 index 000000000000..f560d84ea730 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/event.test.js @@ -0,0 +1,94 @@ +// Validate status 2xx +pm.test("[POST]::/payments/:id/capture - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/payments/:id/capture - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Validate if response has JSON Body +pm.test("[POST]::/payments/:id/capture - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} + +// Response body should have value "6540" for "amount" +if (jsonData?.amount) { + pm.test( + "[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'", + function () { + pm.expect(jsonData.amount).to.eql(6540); + }, + ); +} + +// Response body should have value "6000" for "amount_received" +if (jsonData?.amount_received) { + pm.test( + "[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'", + function () { + pm.expect(jsonData.amount_received).to.eql(6000); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/request.json new file mode 100644 index 000000000000..9fe257ed85e6 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/request.json @@ -0,0 +1,39 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount_to_capture": 6000, + "statement_descriptor_name": "Joseph", + "statement_descriptor_suffix": "JS" + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "capture"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Capture/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/event.test.js new file mode 100644 index 000000000000..d683186aa007 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_capture" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_capture'", + function () { + pm.expect(jsonData.status).to.eql("requires_capture"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json new file mode 100644 index 000000000000..90b6e3bd0385 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/request.json @@ -0,0 +1,87 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "manual", + "capture_on": "2022-09-10T10:11:12Z", + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..92af2088cd92 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'", + function () { + pm.expect(jsonData.status).to.eql("partially_captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js new file mode 100644 index 000000000000..07721f97af31 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/event.test.js @@ -0,0 +1,58 @@ +// Validate status 4xx +pm.test("[POST]::/refunds - Status code is 4xx", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/refunds - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id +if (jsonData?.refund_id) { + pm.collectionVariables.set("refund_id", jsonData.refund_id); + console.log( + "- use {{refund_id}} as collection variable for value", + jsonData.refund_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.", + ); +} + +// Response body should have "error" +pm.test( + "[POST]::/payments/:id/confirm - Content check if 'error' exists", + function () { + pm.expect(typeof jsonData.error !== "undefined").to.be.true; + }, +); + +// Response body should have value "invalid_request" for "error type" +if (jsonData?.error?.type) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'", + function () { + pm.expect(jsonData.error.type).to.eql("invalid_request"); + }, + ); +} + +// Response body should have value "The refund amount exceeds the amount captured" for "error message" +if (jsonData?.error?.type) { + pm.test( + "[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'", + function () { + pm.expect(jsonData.error.message).to.eql("The refund amount exceeds the amount captured"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json new file mode 100644 index 000000000000..5f4c58816d58 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/request.json @@ -0,0 +1,38 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_id": "{{payment_id}}", + "amount": 6540, + "reason": "Customer returned product", + "refund_type": "instant", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": ["{{baseUrl}}"], + "path": ["refunds"] + }, + "description": "To create a refund against an already processed payment" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Variation Cases/Scenario10-Refund exceeds amount captured/Refunds - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 330661b231d5..797aca78a887 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -13610,10 +13610,10 @@ "// Response body should have value \"invalid_request\" for \"error type\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'\",", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"Refund amount exceeds the payment amount\",", + " \"The refund amount exceeds the amount captured\",", " );", " },", " );", diff --git a/postman/collection-json/bluesnap.postman_collection.json b/postman/collection-json/bluesnap.postman_collection.json index 0da43b54cdcd..fa6c9258b8d0 100644 --- a/postman/collection-json/bluesnap.postman_collection.json +++ b/postman/collection-json/bluesnap.postman_collection.json @@ -4927,9 +4927,9 @@ "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.error.message) {", " pm.test(", - " \"[POST]::/refunds - Content check if value for 'message' matches 'Refund amount exceeds the payment amount'\",", + " \"[POST]::/refunds - Content check if value for 'message' matches 'The refund amount exceeds the amount captured'\",", " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Refund amount exceeds the payment amount\");", + " pm.expect(jsonData.error.message).to.eql(\"The refund amount exceeds the amount captured\");", " },", " );", "}", diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index d901fc39b91e..5d3345e57544 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -13364,13 +13364,13 @@ " );", "}", "", - "// Response body should have value \"Refund amount exceeds the payment amount\" for \"message\"", + "// Response body should have value \"The refund amount exceeds the amount captured\" for \"message\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"Refund amount exceeds the payment amount\",", + " \"The refund amount exceeds the amount captured\",", " );", " },", " );", diff --git a/postman/portman-config.json b/postman/portman-config.json index 599767fd6f17..7f94c31a5212 100644 --- a/postman/portman-config.json +++ b/postman/portman-config.json @@ -1583,7 +1583,7 @@ "responseBodyTests": [ { "key": "message", - "value": "Refund amount exceeds the payment amount" + "value": "The refund amount exceeds the amount captured" } ] } From 4d19d8b1d18f49f02e951c5025d35cf5d62cec1b Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:13:46 +0530 Subject: [PATCH 195/443] refactor(payments): add support for receiving card_holder_name field as an empty string (#3127) --- crates/router/src/core/payments/helpers.rs | 58 +++++-------------- .../payments/operations/payment_confirm.rs | 2 - .../payments/operations/payment_create.rs | 2 - .../payments/operations/payment_update.rs | 2 - .../Payments - Confirm/event.test.js | 24 +++----- .../Payments - Retrieve/event.test.js | 8 +-- .../Payments - Confirm/event.test.js | 24 +++----- .../Payments - Retrieve/event.test.js | 8 +-- 8 files changed, 37 insertions(+), 91 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 47d9c0820d45..341699c09251 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -946,42 +946,6 @@ pub fn verify_mandate_details( ) } -// This function validates card_holder_name field to be either null or a non-empty string -pub fn validate_card_holder_name( - payment_method_data: Option, -) -> CustomResult<(), errors::ApiErrorResponse> { - if let Some(pmd) = payment_method_data { - match pmd { - // This validation would occur during payments create - api::PaymentMethodData::Card(card) => { - if let Some(name) = &card.card_holder_name { - if name.clone().expose().is_empty() { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "card_holder_name cannot be empty".to_string(), - }) - .into_report(); - } - } - } - - // This validation would occur during payments confirm - api::PaymentMethodData::CardToken(card) => { - if let Some(name) = card.card_holder_name { - if name.expose().is_empty() { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "card_holder_name cannot be empty".to_string(), - }) - .into_report(); - } - } - } - _ => (), - } - } - - Ok(()) -} - #[instrument(skip_all)] pub fn payment_attempt_status_fsm( payment_method_data: &Option, @@ -1441,13 +1405,15 @@ pub async fn retrieve_payment_method_with_temporary_token( let mut is_card_updated = false; // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked - // from payment_method.card_token object + // from payment_method_data.card_token object let name_on_card = if let Some(name) = card.card_holder_name.clone() { - if name.expose().is_empty() { - card_token_data.and_then(|token_data| { - is_card_updated = true; - token_data.card_holder_name.clone() - }) + if name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) + .or(Some(name)) } else { card.card_holder_name.clone() } @@ -1528,10 +1494,12 @@ pub async fn retrieve_card_with_permanent_token( .attach_printable("failed to fetch card information from the permanent locker")?; // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked - // from payment_method.card_token object + // from payment_method_data.card_token object let name_on_card = if let Some(name) = card.name_on_card.clone() { - if name.expose().is_empty() { - card_token_data.and_then(|token_data| token_data.card_holder_name.clone()) + if name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .or(Some(name)) } else { card.name_on_card } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 8481cd5c8360..95556ac1e0b7 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -872,8 +872,6 @@ impl ValidateRequest ValidateRequest ValidateRequest Date: Thu, 14 Dec 2023 11:38:14 +0530 Subject: [PATCH 196/443] feat(core): enable surcharge support for all connectors (#3109) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 4 +-- .../src/payments/payment_attempt.rs | 10 +++++++ crates/router/src/connector/paypal.rs | 9 +----- crates/router/src/connector/trustpay.rs | 20 ++----------- crates/router/src/connector/utils.rs | 29 +++++++++++++++++-- .../surcharge_decision_configs.rs | 1 + crates/router/src/core/payments.rs | 12 +++++--- .../src/core/payments/flows/authorize_flow.rs | 6 ---- .../payments/operations/payment_approve.rs | 2 +- .../payments/operations/payment_cancel.rs | 2 +- .../payments/operations/payment_capture.rs | 2 +- .../operations/payment_complete_authorize.rs | 2 +- .../payments/operations/payment_confirm.rs | 6 ++-- .../payments/operations/payment_create.rs | 10 ++++++- .../payments/operations/payment_reject.rs | 2 +- .../payments/operations/payment_response.rs | 5 +--- .../payments/operations/payment_session.rs | 2 +- .../core/payments/operations/payment_start.rs | 2 +- .../payments/operations/payment_status.rs | 2 +- .../payments/operations/payment_update.rs | 22 +++++++++++--- .../payments_incremental_authorization.rs | 2 +- .../router/src/core/payments/transformers.rs | 28 +++++++++++++++--- crates/router/src/core/payments/types.rs | 9 ++++-- crates/router/src/services/api.rs | 4 --- crates/router/src/types.rs | 10 ++++++- 25 files changed, 131 insertions(+), 72 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index ef0ae3a15ce6..717a908b1f0d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -322,9 +322,7 @@ impl PaymentsRequest { pub fn get_total_capturable_amount(&self) -> Option { let surcharge_amount = self .surcharge_details - .map(|surcharge_details| { - surcharge_details.surcharge_amount + surcharge_details.tax_amount.unwrap_or(0) - }) + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) .unwrap_or(0); self.amount .map(|amount| i64::from(amount) + surcharge_amount) diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index f7b849f1d4e1..61e48cb64e9a 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -156,6 +156,16 @@ pub struct PaymentAttempt { pub unified_message: Option, } +impl PaymentAttempt { + pub fn get_total_amount(&self) -> i64 { + self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0) + } + pub fn get_total_surcharge_amount(&self) -> Option { + self.surcharge_amount + .map(|surcharge_amount| surcharge_amount + self.tax_amount.unwrap_or(0)) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaymentListFilters { pub connector: Vec, diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 334a58040261..7a9799d415dd 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -265,10 +265,6 @@ impl ConnectorValidation for Paypal { ), } } - - fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Ok(()) - } } impl @@ -423,10 +419,7 @@ impl ConnectorIntegration CustomResult<(), errors::ConnectorError> { - Ok(()) - } -} +impl ConnectorValidation for Trustpay {} impl api::Payment for Trustpay {} @@ -432,12 +428,7 @@ impl _connectors: &settings::Connectors, ) -> CustomResult { let currency = req.request.get_currency()?; - let amount = req - .request - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.final_amount) - .unwrap_or(req.request.get_amount()?); + let amount = req.request.get_amount()?; let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), currency, @@ -544,12 +535,7 @@ impl ConnectorIntegration CustomResult { - let amount = req - .request - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.final_amount) - .unwrap_or(req.request.amount); + let amount = req.request.amount; let connector_router_data = trustpay::TrustpayRouterData::try_from(( &self.get_currency_unit(), req.request.currency, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 9283ff41f73a..d06caf3ae202 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -28,8 +28,8 @@ use crate::{ }, pii::PeekInterface, types::{ - self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, - ApplePayPredecryptData, BrowserInformation, PaymentsCancelData, ResponseId, + self, api, transformers::ForeignTryFrom, ApplePayPredecryptData, BrowserInformation, + PaymentsCancelData, ResponseId, }, utils::{OptionExt, ValueExt}, }; @@ -367,6 +367,10 @@ pub trait PaymentsAuthorizeRequestData { fn get_connector_mandate_id(&self) -> Result; fn get_complete_authorize_url(&self) -> Result; fn get_ip_address_as_optional(&self) -> Option>; + fn get_original_amount(&self) -> i64; + fn get_surcharge_amount(&self) -> Option; + fn get_tax_on_surcharge_amount(&self) -> Option; + fn get_total_surcharge_amount(&self) -> Option; } pub trait PaymentMethodTokenizationRequestData { @@ -473,6 +477,27 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { .map(|ip| Secret::new(ip.to_string())) }) } + fn get_original_amount(&self) -> i64 { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.original_amount) + .unwrap_or(self.amount) + } + fn get_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount) + } + fn get_tax_on_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) + } + fn get_total_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + } } pub trait ConnectorCustomerData { diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index e130795e945a..db1064b36a7a 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -310,6 +310,7 @@ fn get_surcharge_details_from_surcharge_output( .transpose()? .unwrap_or(0); Ok(types::SurchargeDetails { + original_amount: payment_attempt.amount, surcharge: match surcharge_details.surcharge { surcharge_decision_configs::SurchargeOutput::Fixed { amount } => { common_utils_types::Surcharge::Fixed(amount) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 3c7ac0fd78d0..3f815f16be4a 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -204,10 +204,6 @@ where ); if should_continue_transaction { - operation - .to_domain()? - .populate_payment_data(state, &mut payment_data, &merchant_account) - .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -484,6 +480,13 @@ where .surcharge_applicable .unwrap_or(false) { + if let Some(surcharge_details) = payment_data.payment_attempt.get_surcharge_details() { + // if retry payment, surcharge would have been populated from the previous attempt. Use the same surcharge + let surcharge_details = + types::SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)); + payment_data.surcharge_details = Some(surcharge_details); + return Ok(()); + } let raw_card_key = payment_data .payment_method_data .as_ref() @@ -562,6 +565,7 @@ where payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( types::SurchargeDetails { + original_amount: payment_data.payment_attempt.amount, surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 4ef23f481a2c..c934c7c2cd67 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -76,12 +76,6 @@ impl Feature for types::PaymentsAu .connector .validate_capture_method(self.request.capture_method) .to_payment_failed_response()?; - if self.request.surcharge_details.is_some() { - connector - .connector - .validate_if_surcharge_implemented() - .to_payment_failed_response()?; - } if self.should_proceed_with_authorize() { self.decide_authentication_type(); diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 7d0ec0718c25..1a6945b09c84 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -139,7 +139,7 @@ impl payment_method_type.or(payment_attempt.payment_method_type); payment_attempt.payment_experience = request.payment_experience; currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); helpers::validate_customer_id_mandatory_cases( request.setup_future_usage.is_some(), diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 7e6572ff07dc..9810980cd34a 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -102,7 +102,7 @@ impl .await?; let currency = payment_attempt.currency.get_required_value("currency")?; - let amount = payment_attempt.amount.into(); + let amount = payment_attempt.get_total_amount().into(); payment_attempt.cancellation_reason = request.cancellation_reason.clone(); diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 19998a9a0a71..3986b16ce353 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -124,7 +124,7 @@ impl currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 3e91a09ab54e..abb08d14d927 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -135,7 +135,7 @@ impl .payment_experience .or(payment_attempt.payment_experience); currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); helpers::validate_customer_id_mandatory_cases( request.setup_future_usage.is_some(), diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 95556ac1e0b7..58983f264c70 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -377,7 +377,7 @@ impl payment_attempt.capture_method = request.capture_method.or(payment_attempt.capture_method); currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); helpers::validate_customer_id_mandatory_cases( request.setup_future_usage.is_some(), @@ -732,7 +732,7 @@ impl m_db.update_payment_attempt_with_attempt_id( m_payment_data_payment_attempt, storage::PaymentAttemptUpdate::ConfirmUpdate { - amount: payment_data.amount.into(), + amount: payment_data.payment_attempt.amount, currency: payment_data.currency, status: attempt_status, payment_method, @@ -780,7 +780,7 @@ impl m_db.update_payment_intent( m_payment_data_payment_intent, storage::PaymentIntentUpdate::Update { - amount: payment_data.amount.into(), + amount: payment_data.payment_intent.amount, currency: payment_data.currency, setup_future_usage, status: intent_status, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 97cd59e632d1..798678e64df3 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -297,7 +297,7 @@ impl .map(|(payment_method_data, additional_payment_data)| { payment_method_data.apply_additional_payment_data(additional_payment_data) }); - + let amount = payment_attempt.get_total_amount().into(); let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -643,6 +643,12 @@ impl PaymentCreate { } else { utils::get_payment_attempt_id(payment_id, 1) }; + let surcharge_amount = request + .surcharge_details + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = request + .surcharge_details + .and_then(|surcharge_details| surcharge_details.tax_amount); Ok(( storage::PaymentAttemptNew { @@ -668,6 +674,8 @@ impl PaymentCreate { payment_token: request.payment_token.clone(), mandate_id: request.mandate_id.clone(), business_sub_label: request.business_sub_label.clone(), + surcharge_amount, + tax_amount, mandate_details: request .mandate_data .as_ref() diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 5cb3c95dc257..37c7dfd1bae6 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -100,7 +100,7 @@ impl .await?; let currency = payment_attempt.currency.get_required_value("currency")?; - let amount = payment_attempt.amount.into(); + let amount = payment_attempt.get_total_amount().into(); let frm_response = db .find_fraud_check_by_payment_id(payment_intent.payment_id.clone(), merchant_account.merchant_id.clone()) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index f92487d74a7b..8b301c525fd7 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -24,10 +24,7 @@ use crate::{ services::RedirectForm, types::{ self, api, - storage::{ - self, enums, - payment_attempt::{AttemptStatusExt, PaymentAttemptExt}, - }, + storage::{self, enums, payment_attempt::AttemptStatusExt}, transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 7d9c37339349..6f7dcd6c11a7 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -93,7 +93,7 @@ impl payment_attempt.payment_method = Some(storage_enums::PaymentMethod::Wallet); - let amount = payment_intent.amount.into(); + let amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 67c8579d263a..8896e4e43efe 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -89,7 +89,7 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 44fbdf107818..c5311ce3d035 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -244,7 +244,7 @@ async fn get_tracker_for_sync< let payment_id_str = payment_attempt.payment_id.clone(); currency = payment_attempt.currency.get_required_value("currency")?; - amount = payment_attempt.amount.into(); + amount = payment_attempt.get_total_amount().into(); let shipping_address = helpers::get_address_by_id( db, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 0440d5bc0e70..153ded14f4b8 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{enums::FrmSuggestion, payments::RequestSurchargeDetails}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use error_stack::{report, IntoReport, ResultExt}; @@ -281,11 +281,25 @@ impl }) .await .transpose()?; - let next_operation: BoxedOperation<'a, F, api::PaymentsRequest, Ctx> = + let (next_operation, amount): (BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, _) = if request.confirm.unwrap_or(false) { - Box::new(operations::PaymentConfirm) + let amount = { + let amount = request + .amount + .map(Into::into) + .unwrap_or(payment_attempt.amount); + payment_attempt.amount = amount; + payment_intent.amount = amount; + let surcharge_amount = request + .surcharge_details + .as_ref() + .map(RequestSurchargeDetails::get_total_surcharge_amount) + .or(payment_attempt.get_total_surcharge_amount()); + (amount + surcharge_amount.unwrap_or(0)).into() + }; + (Box::new(operations::PaymentConfirm), amount) } else { - Box::new(self) + (Box::new(self), amount) }; payment_intent.status = match request.payment_method_data.as_ref() { diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 3a0dfd19a650..f707fe3a1dad 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -92,7 +92,7 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let currency = payment_attempt.currency.get_required_value("currency")?; - let amount = payment_attempt.amount; + let amount = payment_attempt.get_total_amount(); let profile_id = payment_intent .profile_id diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 2aaf0b2957f6..f0d8c9fd7552 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1038,6 +1038,11 @@ impl TryFrom> for types::PaymentsAuthoriz None } }); + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); Ok(Self { payment_method_data: payment_method_data.get_required_value("payment_method_data")?, setup_future_usage: payment_data.payment_intent.setup_future_usage, @@ -1048,7 +1053,7 @@ impl TryFrom> for types::PaymentsAuthoriz statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, statement_descriptor: payment_data.payment_intent.statement_descriptor_name, capture_method: payment_data.payment_attempt.capture_method, - amount: payment_data.amount.into(), + amount, currency: payment_data.currency, browser_info, email: payment_data.email, @@ -1301,9 +1306,14 @@ impl TryFrom> for types::PaymentsSessionD .collect::, _>>() }) .transpose()?; + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); Ok(Self { - amount: payment_data.amount.into(), + amount, currency: payment_data.currency, country: payment_data.address.billing.and_then(|billing_address| { billing_address.address.and_then(|address| address.country) @@ -1425,6 +1435,11 @@ impl TryFrom> for types::CompleteAuthoriz payload: redirect.json_payload, } }); + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); Ok(Self { setup_future_usage: payment_data.payment_intent.setup_future_usage, @@ -1434,7 +1449,7 @@ impl TryFrom> for types::CompleteAuthoriz confirm: payment_data.payment_attempt.confirm, statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, capture_method: payment_data.payment_attempt.capture_method, - amount: payment_data.amount.into(), + amount, currency: payment_data.currency, browser_info, email: payment_data.email, @@ -1499,12 +1514,17 @@ impl TryFrom> for types::PaymentsPreProce .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })?; + let amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount) + .unwrap_or(payment_data.amount.into()); Ok(Self { payment_method_data, email: payment_data.email, currency: Some(payment_data.currency), - amount: Some(payment_data.amount.into()), + amount: Some(amount), payment_method_type: payment_data.payment_attempt.payment_method_type, setup_mandate_details: payment_data.setup_mandate, capture_method: payment_data.payment_attempt.capture_method, diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index 001082d2c92e..00160db9855f 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -178,6 +178,8 @@ impl MultipleCaptureData { #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct SurchargeDetails { + /// original_amount + pub original_amount: i64, /// surcharge value pub surcharge: common_types::Surcharge, /// tax on surcharge value @@ -198,6 +200,7 @@ impl From<(&RequestSurchargeDetails, &PaymentAttempt)> for SurchargeDetails { let surcharge_amount = request_surcharge_details.surcharge_amount; let tax_on_surcharge_amount = request_surcharge_details.tax_amount.unwrap_or(0); Self { + original_amount: payment_attempt.amount, surcharge: common_types::Surcharge::Fixed(request_surcharge_details.surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -219,13 +222,15 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe currency.to_currency_base_unit_asf64(surcharge_details.tax_on_surcharge_amount)?; let display_final_amount = currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; + let display_total_surcharge_amount = currency.to_currency_base_unit_asf64( + surcharge_details.surcharge_amount + surcharge_details.tax_on_surcharge_amount, + )?; Ok(Self { surcharge: surcharge_details.surcharge.clone().into(), tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into), display_surcharge_amount, display_tax_on_surcharge_amount, - display_total_surcharge_amount: display_surcharge_amount - + display_tax_on_surcharge_amount, + display_total_surcharge_amount, display_final_amount, }) } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 7680ff29d45c..ea254ee4fabf 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -100,10 +100,6 @@ pub trait ConnectorValidation: ConnectorCommon { fn is_webhook_source_verification_mandatory(&self) -> bool { false } - - fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented(format!("Surcharge for {}", self.id())).into()) - } } #[async_trait::async_trait] diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index ecbea1e793c6..cc14fe36a044 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -38,7 +38,7 @@ use crate::{ payments::{types, PaymentData, RecurringMandatePaymentData}, }, services, - types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, + types::transformers::ForeignFrom, utils::OptionExt, }; @@ -373,6 +373,14 @@ pub struct PayoutsFulfillResponseData { #[derive(Debug, Clone)] pub struct PaymentsAuthorizeData { pub payment_method_data: payments::PaymentMethodData, + /// total amount (original_amount + surcharge_amount + tax_on_surcharge_amount) + /// If connector supports separate field for surcharge amount, consider using below functions defined on `PaymentsAuthorizeData` to fetch original amount and surcharge amount separately + /// ``` + /// get_original_amount() + /// get_surcharge_amount() + /// get_tax_on_surcharge_amount() + /// get_total_surcharge_amount() // returns surcharge_amount + tax_on_surcharge_amount + /// ``` pub amount: i64, pub email: Option, pub currency: storage_enums::Currency, From a5618cd5d6eb5b007f7927f05e777e875195a678 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 07:28:44 +0000 Subject: [PATCH 197/443] test(postman): update postman collection files --- .../bankofamerica.postman_collection.json | 12 +- .../stripe.postman_collection.json | 531 +++++++++++++++++- .../trustpay.postman_collection.json | 34 +- 3 files changed, 527 insertions(+), 50 deletions(-) diff --git a/postman/collection-json/bankofamerica.postman_collection.json b/postman/collection-json/bankofamerica.postman_collection.json index cfef0bd7a2f0..752b77dcd150 100644 --- a/postman/collection-json/bankofamerica.postman_collection.json +++ b/postman/collection-json/bankofamerica.postman_collection.json @@ -2431,7 +2431,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -2682,7 +2682,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3532,7 +3532,7 @@ "language": "json" } }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"3566111111111113\",\"card_exp_month\":\"12\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" }, "url": { "raw": "{{baseUrl}}/payments", @@ -3619,12 +3619,12 @@ " );", "}", "", - "// Response body should have value \"processing\" for \"status\"", + "// Response body should have value \"partially_captured\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", " },", " );", "}", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 82ffad6f2f59..e158ccd1a5eb 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -7270,9 +7270,9 @@ "listen": "test", "script": { "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", @@ -7335,24 +7335,15 @@ " );", "}", "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } @@ -7499,16 +7490,15 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } @@ -20307,6 +20297,503 @@ { "name": "Variation Cases", "item": [ + { + "name": "Scenario10-Refund exceeds amount captured", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"The refund amount exceeds the amount captured\" for \"error message\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"The refund amount exceeds the amount captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with Invalid card details", "item": [ diff --git a/postman/collection-json/trustpay.postman_collection.json b/postman/collection-json/trustpay.postman_collection.json index 37c7fecf7ec9..cacc015851d8 100644 --- a/postman/collection-json/trustpay.postman_collection.json +++ b/postman/collection-json/trustpay.postman_collection.json @@ -938,9 +938,9 @@ "listen": "test", "script": { "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", "});", "", "// Validate if response header has matching content-type", @@ -1003,24 +1003,15 @@ " );", "}", "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } @@ -1158,16 +1149,15 @@ " );", "}", "", - "// Response body should have value \"requires_payment_method\" for \"status\"", + "// Response body should have value \"succeeded\" for \"status\"", "if (jsonData?.status) {", " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_payment_method'\",", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", " },", " );", - "}", - "" + "}" ], "type": "text/javascript" } From a8fdfc4c64888adb6a221c81d4e9637468e1b414 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 07:28:45 +0000 Subject: [PATCH 198/443] chore(version): v1.100.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d604bc8007..2b041a8b3492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.100.0 (2023-12-14) + +### Features + +- **connector:** + - [RISKIFIED] Add support for riskified frm connector ([#2533](https://github.com/juspay/hyperswitch/pull/2533)) ([`151a30f`](https://github.com/juspay/hyperswitch/commit/151a30f4eed10924cd93bf7f4f66976af0ab8314)) + - [HELCIM] Add connector_request_reference_id in invoice_number ([#3087](https://github.com/juspay/hyperswitch/pull/3087)) ([`3cc9642`](https://github.com/juspay/hyperswitch/commit/3cc9642f3ac4c07fb675e9ff4032832819d877a1)) +- **core:** Enable surcharge support for all connectors ([#3109](https://github.com/juspay/hyperswitch/pull/3109)) ([`57e1ae9`](https://github.com/juspay/hyperswitch/commit/57e1ae9dea6ff70fb1bca47c479c35026c167bad)) +- **events:** Add type info to outgoing requests & maintain structural & PII type info ([#2956](https://github.com/juspay/hyperswitch/pull/2956)) ([`6e82b0b`](https://github.com/juspay/hyperswitch/commit/6e82b0bd746b405281f79b86a3cd92b550a33f68)) +- **external_services:** Adds encrypt function for KMS ([#3111](https://github.com/juspay/hyperswitch/pull/3111)) ([`bca7cdb`](https://github.com/juspay/hyperswitch/commit/bca7cdb4c14b5fbb40d8cbf59fd1756ad27ac674)) + +### Bug Fixes + +- **api_locking:** Fix the unit interpretation for `LockSettings` expiry ([#3121](https://github.com/juspay/hyperswitch/pull/3121)) ([`3f4167d`](https://github.com/juspay/hyperswitch/commit/3f4167dbd477c793e1a4cc572da0c12d66f2b649)) +- **connector:** [trustpay] make paymentId optional field ([#3101](https://github.com/juspay/hyperswitch/pull/3101)) ([`62a7c30`](https://github.com/juspay/hyperswitch/commit/62a7c3053c5e276091f5bd54a5679caef58a4ace)) +- **docker-compose:** Remove label list from docker compose yml ([#3118](https://github.com/juspay/hyperswitch/pull/3118)) ([`e1e23fd`](https://github.com/juspay/hyperswitch/commit/e1e23fd987cae96e56311d1cfdcb225d9327860c)) +- Validate refund amount with amount_captured instead of amount ([#3120](https://github.com/juspay/hyperswitch/pull/3120)) ([`be13d15`](https://github.com/juspay/hyperswitch/commit/be13d15d3c0214c863e131cf1dbe184d5baec5d7)) + +### Refactors + +- **connector:** [Wise] Error Message For Connector Implementation ([#2952](https://github.com/juspay/hyperswitch/pull/2952)) ([`1add2c0`](https://github.com/juspay/hyperswitch/commit/1add2c059f4fb5653f33e2f3ce454793caf2d595)) +- **payments:** Add support for receiving card_holder_name field as an empty string ([#3127](https://github.com/juspay/hyperswitch/pull/3127)) ([`4d19d8b`](https://github.com/juspay/hyperswitch/commit/4d19d8b1d18f49f02e951c5025d35cf5d62cec1b)) + +### Testing + +- **postman:** Update postman collection files ([`a5618cd`](https://github.com/juspay/hyperswitch/commit/a5618cd5d6eb5b007f7927f05e777e875195a678)) + +**Full Changelog:** [`v1.99.0...v1.100.0`](https://github.com/juspay/hyperswitch/compare/v1.99.0...v1.100.0) + +- - - + + ## 1.99.0 (2023-12-12) ### Features From 70b86b71e4809d2a47c6bc1214f72c37d3325c37 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Thu, 14 Dec 2023 16:45:10 +0530 Subject: [PATCH 199/443] fix(locker): fix double serialization for json request (#3134) --- .../src/core/payment_methods/transformers.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 71c2cf8f2003..5506dc7eb9ac 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -313,9 +313,6 @@ pub async fn mk_add_locker_request_hs<'a>( .change_context(errors::VaultError::RequestEncodingFailed)?; let jwe_payload = mk_basilisk_req(jwekey, &jws, locker_choice).await?; - - let body = utils::Encode::::encode_to_value(&jwe_payload) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = match locker_choice { api_enums::LockerChoice::Basilisk => locker.host.to_owned(), api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), @@ -323,7 +320,7 @@ pub async fn mk_add_locker_request_hs<'a>( url.push_str("/cards/add"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(RequestContent::Json(Box::new(body.to_string()))); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); Ok(request) } @@ -459,9 +456,6 @@ pub async fn mk_get_card_request_hs( let target_locker = locker_choice.unwrap_or(api_enums::LockerChoice::Basilisk); let jwe_payload = mk_basilisk_req(jwekey, &jws, target_locker).await?; - - let body = utils::Encode::::encode_to_value(&jwe_payload) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = match target_locker { api_enums::LockerChoice::Basilisk => locker.host.to_owned(), api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), @@ -469,7 +463,7 @@ pub async fn mk_get_card_request_hs( url.push_str("/cards/retrieve"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(RequestContent::Json(Box::new(body.to_string()))); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); Ok(request) } @@ -537,13 +531,11 @@ pub async fn mk_delete_card_request_hs( let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; - let body = utils::Encode::::encode_to_value(&jwe_payload) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/cards/delete"); let mut request = services::Request::new(services::Method::Post, &url); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(RequestContent::Json(Box::new(body.to_string()))); + request.set_body(RequestContent::Json(Box::new(jwe_payload))); Ok(request) } @@ -602,14 +594,12 @@ pub fn mk_crud_locker_request( path: &str, req: api::TokenizePayloadEncrypted, ) -> CustomResult { - let body = utils::Encode::::encode_to_value(&req) - .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.basilisk_host.to_owned(); url.push_str(path); let mut request = services::Request::new(services::Method::Post, &url); request.add_default_headers(); request.add_header(headers::CONTENT_TYPE, "application/json".into()); - request.set_body(RequestContent::Json(Box::new(body.to_string()))); + request.set_body(RequestContent::Json(Box::new(req))); Ok(request) } From 1f848659f135542fdfa967b3b48ad6cdf69fda2c Mon Sep 17 00:00:00 2001 From: Shanks Date: Thu, 14 Dec 2023 16:51:34 +0530 Subject: [PATCH 200/443] fix(router): add routing cache invalidation on payment connector update (#3132) --- crates/euclid_wasm/Cargo.toml | 2 +- crates/router/src/core/admin.rs | 77 ++++--------------- .../src/types/domain/merchant_account.rs | 5 ++ 3 files changed, 23 insertions(+), 61 deletions(-) diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 51288a9d0feb..f17cdec8759a 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true crate-type = ["cdylib"] [features] -default = ["connector_choice_bcompat"] +default = ["connector_choice_bcompat", "connector_choice_mca_id"] release = ["connector_choice_bcompat", "connector_choice_mca_id"] connector_choice_bcompat = ["api_models/connector_choice_bcompat"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index f6aaf22480b9..84a2f442de8f 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -776,14 +776,13 @@ pub async fn create_payment_connector( let pm_auth_connector = api_enums::convert_pm_auth_connector(req.connector_name.to_string().as_str()); - let is_unroutable_connector = if pm_auth_connector.is_some() { + if pm_auth_connector.is_some() { if req.connector_type != api_enums::ConnectorType::PaymentMethodAuth { return Err(errors::ApiErrorResponse::InvalidRequestData { message: "Invalid connector type given".to_string(), }) .into_report(); } - true } else { let routable_connector_option = req .connector_name @@ -794,7 +793,6 @@ pub async fn create_payment_connector( message: "Invalid connector name given".to_string(), })?; routable_connector = Some(routable_connector_option); - false }; // If connector label is not passed in the request, generate one @@ -863,31 +861,13 @@ pub async fn create_payment_connector( // The purpose of this merchant account update is just to update the // merchant account `modified_at` field for KGraph cache invalidation - let merchant_account_update = storage::MerchantAccountUpdate::Update { - merchant_name: None, - merchant_details: None, - return_url: None, - webhook_details: None, - sub_merchants_enabled: None, - parent_merchant_id: None, - enable_payment_response_hash: None, - locker_id: None, - payment_response_hash_key: None, - primary_business_details: None, - metadata: None, - publishable_key: None, - redirect_to_merchant_with_http_post: None, - routing_algorithm: None, - intent_fulfillment_time: None, - frm_routing_algorithm: None, - payout_routing_algorithm: None, - default_profile: None, - payment_link_config: None, - }; - state .store - .update_specific_fields_in_merchant(merchant_id, merchant_account_update, &key_store) + .update_specific_fields_in_merchant( + merchant_id, + storage::MerchantAccountUpdate::ModifiedAtUpdate, + &key_store, + ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error updating the merchant account when creating payment connector")?; @@ -1018,40 +998,6 @@ pub async fn create_payment_connector( ], ); - if !is_unroutable_connector { - if let Some(routable_connector_val) = routable_connector { - let choice = routing_types::RoutableConnectorChoice { - #[cfg(feature = "backwards_compatibility")] - choice_kind: routing_types::RoutableChoiceKind::FullStruct, - connector: routable_connector_val, - #[cfg(feature = "connector_choice_mca_id")] - merchant_connector_id: Some(mca.merchant_connector_id.clone()), - #[cfg(not(feature = "connector_choice_mca_id"))] - sub_label: req.business_sub_label.clone(), - }; - - if !default_routing_config.contains(&choice) { - default_routing_config.push(choice.clone()); - routing_helpers::update_merchant_default_config( - &*state.clone().store, - merchant_id, - default_routing_config, - ) - .await?; - } - - if !default_routing_config_for_profile.contains(&choice) { - default_routing_config_for_profile.push(choice); - routing_helpers::update_merchant_default_config( - &*state.store, - &profile_id, - default_routing_config_for_profile, - ) - .await?; - } - } - }; - let mca_response = mca.try_into()?; Ok(service_api::ApplicationResponse::Json(mca_response)) } @@ -1240,6 +1186,17 @@ pub async fn update_payment_connector( } } + // The purpose of this merchant account update is just to update the + // merchant account `modified_at` field for KGraph cache invalidation + db.update_specific_fields_in_merchant( + merchant_id, + storage::MerchantAccountUpdate::ModifiedAtUpdate, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error updating the merchant account when updating payment connector")?; + let payment_connector = storage::MerchantConnectorAccountUpdate::Update { merchant_id: None, connector_type: Some(req.connector_type), diff --git a/crates/router/src/types/domain/merchant_account.rs b/crates/router/src/types/domain/merchant_account.rs index cbcd70aaef17..3832ffb3da7e 100644 --- a/crates/router/src/types/domain/merchant_account.rs +++ b/crates/router/src/types/domain/merchant_account.rs @@ -79,6 +79,7 @@ pub enum MerchantAccountUpdate { recon_status: diesel_models::enums::ReconStatus, }, UnsetDefaultProfile, + ModifiedAtUpdate, } impl From for MerchantAccountUpdateInternal { @@ -140,6 +141,10 @@ impl From for MerchantAccountUpdateInternal { default_profile: Some(None), ..Default::default() }, + MerchantAccountUpdate::ModifiedAtUpdate => Self { + modified_at: Some(date_time::now()), + ..Default::default() + }, } } } From f4578463d5e1a0f442aacebdfa7af0460489ba8c Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:52:48 +0530 Subject: [PATCH 201/443] feat(payments): add outgoing payments webhooks (#3133) --- crates/router/src/core/payments.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 3f815f16be4a..44c46732529e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -89,7 +89,7 @@ pub async fn payments_operation_core( )> where F: Send + Clone + Sync, - Req: Authenticate, + Req: Authenticate + Clone, Op: Operation + Send + Sync, // To create connector flow specific interface data @@ -423,6 +423,23 @@ where .await?; } + let cloned_payment_data = payment_data.clone(); + let cloned_customer = customer.clone(); + let cloned_request = req.clone(); + + crate::utils::trigger_payments_webhook( + merchant_account, + business_profile, + cloned_payment_data, + Some(cloned_request), + cloned_customer, + state, + operation, + ) + .await + .map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error)) + .ok(); + Ok(( payment_data, req, @@ -624,7 +641,7 @@ where F: Send + Clone + Sync, FData: Send + Sync, Op: Operation + Send + Sync + Clone, - Req: Debug + Authenticate, + Req: Debug + Authenticate + Clone, Res: transformers::ToResponse, Op>, // To create connector flow specific interface data PaymentData: ConstructFlowSpecificData, From 71a86a804e15e4d053f92cfddb36a15cf7b77f7a Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:42:58 +0530 Subject: [PATCH 202/443] fix(connector): [CashToCode]Fix cashtocode redirection for evoucher pm type (#3131) Co-authored-by: Nitesh Balla Co-authored-by: Arjun Karthik --- .../src/connector/cashtocode/transformers.rs | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index b38ca4b67132..9aa6286a963f 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; -use common_utils::{ext_traits::ValueExt, pii::Email}; +pub use common_utils::request::Method; +use common_utils::{errors::CustomResult, ext_traits::ValueExt, pii::Email}; use error_stack::{IntoReport, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -185,7 +186,7 @@ pub enum CashtocodePaymentsResponse { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CashtocodePaymentsResponseData { - pub pay_url: String, + pub pay_url: url::Url, } #[derive(Debug, Clone, Deserialize)] @@ -195,17 +196,44 @@ pub struct CashtocodePaymentsSyncResponse { pub amount: i64, } -impl +fn get_redirect_form_data( + payment_method_type: &enums::PaymentMethodType, + response_data: CashtocodePaymentsResponseData, +) -> CustomResult { + match payment_method_type { + enums::PaymentMethodType::ClassicReward => Ok(services::RedirectForm::Form { + //redirect form is manually constructed because the connector for this pm type expects query params in the url + endpoint: response_data.pay_url.to_string(), + method: services::Method::Post, + form_fields: Default::default(), + }), + enums::PaymentMethodType::Evoucher => Ok(services::RedirectForm::from(( + //here the pay url gets parsed, and query params are sent as formfields as the connector expects + response_data.pay_url, + services::Method::Get, + ))), + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("CashToCode"), + ))?, + } +} + +impl TryFrom< - types::ResponseRouterData, - > for types::RouterData + types::ResponseRouterData< + F, + CashtocodePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( item: types::ResponseRouterData< F, CashtocodePaymentsResponse, - T, + types::PaymentsAuthorizeData, types::PaymentsResponseData, >, ) -> Result { @@ -222,11 +250,13 @@ impl }), ), CashtocodePaymentsResponse::CashtoCodeData(response_data) => { - let redirection_data = services::RedirectForm::Form { - endpoint: response_data.pay_url, - method: services::Method::Post, - form_fields: Default::default(), - }; + let payment_method_type = item + .data + .request + .payment_method_type + .as_ref() + .ok_or(errors::ConnectorError::MissingPaymentMethodType)?; + let redirection_data = get_redirect_form_data(payment_method_type, response_data)?; ( enums::AttemptStatus::AuthenticationPending, Ok(types::PaymentsResponseData::TransactionResponse { @@ -272,10 +302,10 @@ impl >, ) -> Result { Ok(Self { - status: enums::AttemptStatus::Charged, + status: enums::AttemptStatus::Charged, // Charged status is hardcoded because cashtocode do not support Psync, and we only receive webhooks when payment is succeeded, this tryFrom is used for CallConnectorAction. response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.data.attempt_id.clone(), + item.data.attempt_id.clone(), //in response they only send PayUrl, so we use attempt_id as connector_transaction_id ), redirection_data: None, mandate_reference: None, From 5dd4540b2cc5420e4e6ccb88028a6fc78f4a3344 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:28:00 +0000 Subject: [PATCH 203/443] chore(version): v1.101.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b041a8b3492..ad610bbafde5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.101.0 (2023-12-14) + +### Features + +- **payments:** Add outgoing payments webhooks ([#3133](https://github.com/juspay/hyperswitch/pull/3133)) ([`f457846`](https://github.com/juspay/hyperswitch/commit/f4578463d5e1a0f442aacebdfa7af0460489ba8c)) + +### Bug Fixes + +- **connector:** [CashToCode]Fix cashtocode redirection for evoucher pm type ([#3131](https://github.com/juspay/hyperswitch/pull/3131)) ([`71a86a8`](https://github.com/juspay/hyperswitch/commit/71a86a804e15e4d053f92cfddb36a15cf7b77f7a)) +- **locker:** Fix double serialization for json request ([#3134](https://github.com/juspay/hyperswitch/pull/3134)) ([`70b86b7`](https://github.com/juspay/hyperswitch/commit/70b86b71e4809d2a47c6bc1214f72c37d3325c37)) +- **router:** Add routing cache invalidation on payment connector update ([#3132](https://github.com/juspay/hyperswitch/pull/3132)) ([`1f84865`](https://github.com/juspay/hyperswitch/commit/1f848659f135542fdfa967b3b48ad6cdf69fda2c)) + +**Full Changelog:** [`v1.100.0...v1.101.0`](https://github.com/juspay/hyperswitch/compare/v1.100.0...v1.101.0) + +- - - + + ## 1.100.0 (2023-12-14) ### Features From d8de3c285c90103da93f0f3fd0241924dabd256f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:27:25 +0530 Subject: [PATCH 204/443] chore(deps): bump zerocopy from 0.7.26 to 0.7.31 (#3136) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b58631632e4d..f15651e7c350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7571,18 +7571,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", From 5b2c3291d4fbe3c4154c187b4e915dc3365e761a Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 15 Dec 2023 16:35:41 +0530 Subject: [PATCH 205/443] fix(connector): [Checkout] Fix status mapping for checkout (#3073) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connector/checkout/transformers.rs | 70 +- .../collection-dir/checkout/.event.meta.json | 5 +- postman/collection-dir/checkout/.info.json | 5 +- postman/collection-dir/checkout/.meta.json | 5 +- .../checkout/Flow Testcases/.meta.json | 6 +- .../Flow Testcases/Happy Cases/.meta.json | 4 + .../.meta.json | 5 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 12 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../3DS Payment/.meta.json | 5 +- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 5 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Cancel/.event.meta.json | 5 +- .../Payments - Cancel/request.json | 10 +- .../Payments - Capture/.event.meta.json | 5 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 5 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Capture/.event.meta.json | 5 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/request.json | 8 +- .../Refunds - Create/.event.meta.json | 5 +- .../Refunds - Create/request.json | 8 +- .../Payments - Capture/.event.meta.json | 5 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve-copy/.event.meta.json | 5 +- .../Payments - Retrieve-copy/request.json | 9 +- .../Payments - Retrieve/.event.meta.json | 5 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Capture - 1/.event.meta.json | 5 +- .../Payments - Capture - 1/request.json | 10 +- .../Payments - Capture - 2/.event.meta.json | 5 +- .../Payments - Capture - 2/request.json | 10 +- .../Payments - Capture - 3/.event.meta.json | 5 +- .../Payments - Capture - 3/request.json | 10 +- .../Payments - Create/.event.meta.json | 5 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 5 +- .../Payments - Retrieve/request.json | 9 +- .../Refunds - Create/.event.meta.json | 5 +- .../Refunds - Create/request.json | 8 +- .../Refunds - Retrieve/.event.meta.json | 5 +- .../Refunds - Retrieve/request.json | 9 +- .../Payments - Create/event.test.js | 6 +- .../Refunds - Create Copy/.event.meta.json | 1 + .../event.test.js | 6 +- .../event.test.js | 8 +- .../.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Confirm/.event.meta.json | 4 +- .../Payments - Confirm/event.test.js | 6 +- .../Payments - Confirm/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 10 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Confirm/.event.meta.json | 4 +- .../Payments - Confirm/event.test.js | 6 +- .../Payments - Confirm/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Capture/.event.meta.json | 4 +- .../Payments - Capture/request.json | 10 +- .../Payments - Confirm/.event.meta.json | 4 +- .../Payments - Confirm/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Capture/.event.meta.json | 4 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Capture/.event.meta.json | 4 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Cancel/.event.meta.json | 4 +- .../Payments - Cancel/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Refunds - Create/.event.meta.json | 5 +- .../Refunds - Create/request.json | 8 +- .../Refunds - Retrieve/.event.meta.json | 4 +- .../Refunds - Retrieve/request.json | 9 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve-copy/.event.meta.json | 4 +- .../Payments - Retrieve-copy/request.json | 9 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Refunds - Create-copy/.event.meta.json | 5 +- .../Refunds - Create-copy/request.json | 8 +- .../Refunds - Create/.event.meta.json | 5 +- .../Refunds - Create/request.json | 8 +- .../Refunds - Retrieve-copy/.event.meta.json | 4 +- .../Refunds - Retrieve-copy/request.json | 9 +- .../Refunds - Retrieve/.event.meta.json | 4 +- .../Refunds - Retrieve/request.json | 9 +- .../API Key - Create/.event.meta.json | 4 +- .../QuickStart/API Key - Create/request.json | 9 +- .../.event.meta.json | 5 +- .../Merchant Account - Create/request.json | 8 +- .../.event.meta.json | 5 +- .../Payment Connector - Create/request.json | 30 +- .../Payments - Create/.event.meta.json | 4 +- .../QuickStart/Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 17 +- .../Refunds - Create/.event.meta.json | 5 +- .../QuickStart/Refunds - Create/request.json | 8 +- .../Refunds - Retrieve/.event.meta.json | 5 +- .../Refunds - Retrieve/request.json | 9 +- .../.event.meta.json | 4 +- .../request.json | 8 +- .../.event.meta.json | 4 +- .../request.json | 8 +- .../.event.meta.json | 4 +- .../request.json | 8 +- .../.event.meta.json | 4 +- .../request.json | 8 +- .../.meta.json | 5 +- .../Payments - Confirm/.event.meta.json | 4 +- .../Payments - Confirm/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Capture/.event.meta.json | 4 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../.meta.json | 5 +- .../Payments - Capture/.event.meta.json | 5 +- .../Payments - Capture/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 8 +- .../.meta.json | 5 +- .../Payments - Cancel/.event.meta.json | 4 +- .../Payments - Cancel/request.json | 10 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 8 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/event.test.js | 6 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Refunds - Create/.event.meta.json | 5 +- .../Refunds - Create/request.json | 8 +- .../Payments - Create/.event.meta.json | 4 +- .../Payments - Create/request.json | 8 +- .../Payments - Retrieve/.event.meta.json | 4 +- .../Payments - Retrieve/request.json | 9 +- .../Refunds - Create/.event.meta.json | 5 +- .../Refunds - Create/request.json | 8 +- .../checkout/Health check/.meta.json | 4 +- .../Health check/New Request/.event.meta.json | 4 +- .../Health check/New Request/request.json | 8 +- .../checkout.postman_collection.json | 27793 ++++++++-------- 185 files changed, 14869 insertions(+), 14202 deletions(-) diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 37c038c22afe..5bd80a10c4b5 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -6,7 +6,10 @@ use time::PrimitiveDateTime; use url::Url; use crate::{ - connector::utils::{self, ApplePayDecrypt, PaymentsCaptureRequestData, RouterData, WalletData}, + connector::utils::{ + self, to_connector_meta, ApplePayDecrypt, PaymentsCaptureRequestData, RouterData, + WalletData, + }, consts, core::errors, services, @@ -241,6 +244,17 @@ pub struct PaymentsRequest { pub reference: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckoutMeta { + pub psync_flow: CheckoutPaymentIntent, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum CheckoutPaymentIntent { + Capture, + Authorize, +} + #[derive(Debug, Serialize)] pub struct CheckoutThreeDS { enabled: bool, @@ -461,7 +475,27 @@ impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enum if capture_method == Some(enums::CaptureMethod::Automatic) || capture_method.is_none() { - Self::Charged + Self::Pending + } else { + Self::Authorized + } + } + CheckoutPaymentStatus::Captured => Self::Charged, + CheckoutPaymentStatus::Declined => Self::Failure, + CheckoutPaymentStatus::Pending => Self::AuthenticationPending, + CheckoutPaymentStatus::CardVerified => Self::Pending, + } + } +} + +impl ForeignFrom<(CheckoutPaymentStatus, CheckoutPaymentIntent)> for enums::AttemptStatus { + fn foreign_from(item: (CheckoutPaymentStatus, CheckoutPaymentIntent)) -> Self { + let (status, psync_flow) = item; + + match status { + CheckoutPaymentStatus::Authorized => { + if psync_flow == CheckoutPaymentIntent::Capture { + Self::Pending } else { Self::Authorized } @@ -533,6 +567,24 @@ pub struct Balances { available_to_capture: i32, } +fn get_connector_meta( + capture_method: enums::CaptureMethod, +) -> CustomResult { + match capture_method { + enums::CaptureMethod::Automatic => Ok(serde_json::json!(CheckoutMeta { + psync_flow: CheckoutPaymentIntent::Capture, + })), + enums::CaptureMethod::Manual | enums::CaptureMethod::ManualMultiple => { + Ok(serde_json::json!(CheckoutMeta { + psync_flow: CheckoutPaymentIntent::Authorize, + })) + } + enums::CaptureMethod::Scheduled => { + Err(errors::ConnectorError::CaptureMethodNotSupported.into()) + } + } +} + impl TryFrom> for types::PaymentsAuthorizeRouterData { @@ -540,6 +592,9 @@ impl TryFrom> fn try_from( item: types::PaymentsResponseRouterData, ) -> Result { + let connector_meta = + get_connector_meta(item.data.request.capture_method.unwrap_or_default())?; + let redirection_data = item.response.links.redirect.map(|href| { services::RedirectForm::from((href.redirection_url, services::Method::Get)) }); @@ -570,7 +625,7 @@ impl TryFrom> resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), redirection_data, mandate_reference: None, - connector_metadata: None, + connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: Some( item.response.reference.unwrap_or(item.response.id), @@ -595,8 +650,10 @@ impl TryFrom> let redirection_data = item.response.links.redirect.map(|href| { services::RedirectForm::from((href.redirection_url, services::Method::Get)) }); + let checkout_meta: CheckoutMeta = + to_connector_meta(item.data.request.connector_meta.clone())?; let status = - enums::AttemptStatus::foreign_from((item.response.status, item.response.balances)); + enums::AttemptStatus::foreign_from((item.response.status, checkout_meta.psync_flow)); let error_response = if status == enums::AttemptStatus::Failure { Some(types::ErrorResponse { status_code: item.http_code, @@ -772,6 +829,9 @@ impl TryFrom> fn try_from( item: types::PaymentsCaptureResponseRouterData, ) -> Result { + let connector_meta = serde_json::json!(CheckoutMeta { + psync_flow: CheckoutPaymentIntent::Capture, + }); let (status, amount_captured) = if item.http_code == 202 { ( enums::AttemptStatus::Charged, @@ -794,7 +854,7 @@ impl TryFrom> resource_id: types::ResponseId::ConnectorTransactionId(resource_id), redirection_data: None, mandate_reference: None, - connector_metadata: None, + connector_metadata: Some(connector_meta), network_txn_id: None, connector_response_reference_id: item.response.reference, incremental_authorization_allowed: None, diff --git a/postman/collection-dir/checkout/.event.meta.json b/postman/collection-dir/checkout/.event.meta.json index eb871bbcb9bb..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/.event.meta.json +++ b/postman/collection-dir/checkout/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.prerequest.js", "event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/.info.json b/postman/collection-dir/checkout/.info.json index d061408c8abb..4c3a1efad421 100644 --- a/postman/collection-dir/checkout/.info.json +++ b/postman/collection-dir/checkout/.info.json @@ -1,9 +1,10 @@ { "info": { - "_postman_id": "0da8c3be-4466-413e-9e72-81b21533423e", + "_postman_id": "9ab8f157-6b4b-430a-9ca8-34931682f988", "name": "Checkout Collection", "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "25737662" + "_exporter_id": "25180378", + "_collection_link": "https://galactic-desert-365744.postman.co/workspace/postman-tests-for-all-connector~f274d40a-132c-47e3-8240-6793392ee4d1/collection/25180378-9ab8f157-6b4b-430a-9ca8-34931682f988?action=share&creator=25180378&source=collection_link" } } diff --git a/postman/collection-dir/checkout/.meta.json b/postman/collection-dir/checkout/.meta.json index d513035ce2d6..91b6a65c5bc6 100644 --- a/postman/collection-dir/checkout/.meta.json +++ b/postman/collection-dir/checkout/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Health check", "Flow Testcases"] + "childrenOrder": [ + "Health check", + "Flow Testcases" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/.meta.json b/postman/collection-dir/checkout/Flow Testcases/.meta.json index 023989e1e494..1bbce843680e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/.meta.json @@ -1,3 +1,7 @@ { - "childrenOrder": ["QuickStart", "Happy Cases", "Variation Cases"] + "childrenOrder": [ + "QuickStart", + "Happy Cases", + "Variation Cases" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json index b2d6cbdc5495..bf036d3b7368 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/.meta.json @@ -1,5 +1,9 @@ { "childrenOrder": [ + "Scenario11-Save card flow", + "Scenario12-Don't Pass CVV for save card flow and verify success payment", + "Scenario13-Pass Invalid CVV for save card flow and verify failed payment", + "Scenario14-Save card payment with manual capture", "Scenario1-Create payment with confirm true", "Scenario2-Create payment with confirm false", "Scenario3-Create payment without PMD", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json index 69b505c6d863..60051ecca220 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js index cb7a76331cd6..30eaca5c0ca0 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index 144a35f773aa..4e96529fe9a9 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -30,11 +30,11 @@ "phone_country_code": "+1", "description": "Its my first payment request", "authentication_type": "no_three_ds", - "return_url": "https://duck.com", + "return_url": "https://google.com", "payment_method": "card", "payment_method_data": { "card": { - "card_number": "4005519200000004", + "card_number": "4485040371536584", "card_exp_month": "10", "card_exp_year": "25", "card_holder_name": "joseph Doe", @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json index 69b505c6d863..60051ecca220 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Retrieve"] + "childrenOrder": [ + "Payments - Create", + "Payments - Retrieve" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json index f5f3112ab99b..197580ddffa4 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json index 3a1d8aa178f2..f64e37a125a2 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Cancel/request.json @@ -23,8 +23,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/cancel", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "cancel"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json index 491b7d68feb6..ff371b247dbe 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Refund After Partial Capture/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json index 38ea8b2b84e9..d10988d49b14 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 1/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 2/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json index e67be3211422..127549802b02 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Capture - 3/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json index 2c085d1319bd..b28dec99902e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Create/request.json @@ -80,8 +80,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json index 38ea8b2b84e9..d10988d49b14 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json index 96cd9f45b0ff..9e0bc932afbe 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js index b7c7c3384d99..74bc3f6ab783 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Create/event.test.js @@ -49,12 +49,12 @@ if (jsonData?.customer_id) { console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.'); }; -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json index 688c85746ef1..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Refunds - Create Copy/.event.meta.json @@ -1,5 +1,6 @@ { "eventOrder": [ + "event.prerequest.js", "event.test.js" ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js index 22d88c07e50a..4f38b75d82fd 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Save card payments - Confirm/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js index c5b17c6e79ad..82a20684b26c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Save card payments - Confirm/event.test.js @@ -63,16 +63,16 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } -// Response body should have value "adyen" for "connector" +// Response body should have value "checkout" for "connector" if (jsonData?.connector) { pm.test( "[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json index 4bacbe0f555f..fc583d47a253 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/.meta.json @@ -6,8 +6,6 @@ "Save card payments - Create", "Save card payments - Confirm", "Payments - Capture", - "Payments - Retrieve-copy", - "Refunds - Create Copy", - "Refunds - Retrieve Copy" + "Payments - Retrieve-copy" ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js index b7c7c3384d99..74bc3f6ab783 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Create/event.test.js @@ -49,12 +49,12 @@ if (jsonData?.customer_id) { console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.'); }; -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js index 8a41b0ef7e5d..bc42544064b8 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json index 98eb4829e4bd..cffc5ba2ed34 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json @@ -43,8 +43,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json index a6a8150c2404..762826dac09d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json @@ -30,7 +30,7 @@ "phone_country_code": "+1", "description": "Its my first payment request", "authentication_type": "no_three_ds", - "return_url": "https://duck.com", + "return_url": "https://google.com", "payment_method": "card", "payment_method_data": { "card": { @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js index 2fd89553c9cc..71acfe97494c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/event.test.js @@ -63,12 +63,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json index dd704a5de76c..cc9d7304aa36 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Confirm/request.json @@ -53,8 +53,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json index e3b1e235042f..b28abd0c3090 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Create/request.json @@ -67,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json index 9fe257ed85e6..8975575ca40e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json index 1c3a9e08ac33..3895e38ff26a 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Confirm/request.json @@ -43,8 +43,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json index 50cb0663b403..de5aa29f7b05 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json index cceb2b55f0a7..8efb99d3c905 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json index 9fe257ed85e6..8975575ca40e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json index 3a1d8aa178f2..f64e37a125a2 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Cancel/request.json @@ -23,8 +23,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/cancel", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "cancel"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js index 2f020d7ff20d..4b3558a20cc5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js index 2f020d7ff20d..4b3558a20cc5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json index caed78185784..b56057fad5dc 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create-copy/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json index 9fe125ce8ea4..d18aaf8befdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve-copy/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json index 6ceefe5d24cd..4e4c66284978 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/API Key - Create/request.json @@ -35,8 +35,13 @@ }, "url": { "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": ["{{baseUrl}}"], - "path": ["api_keys", ":merchant_id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], "variable": [ { "key": "merchant_id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json index dcbf46ee5382..ffeea3410a4c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Merchant Account - Create/request.json @@ -84,8 +84,12 @@ }, "url": { "raw": "{{baseUrl}}/accounts", - "host": ["{{baseUrl}}"], - "path": ["accounts"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] }, "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json index c0f6521a5c2d..9500716c12c9 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -56,12 +56,10 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": ["AmericanExpress", - "Discover", - "Interac", - "JCB", - "Mastercard", - "Visa", "DinersClub","UnionPay","RuPay"], + "card_networks": [ + "Visa", + "Mastercard" + ], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -69,12 +67,10 @@ }, { "payment_method_type": "debit", - "card_networks": ["AmericanExpress", - "Discover", - "Interac", - "JCB", - "Mastercard", - "Visa", "DinersClub","UnionPay","RuPay"], + "card_networks": [ + "Visa", + "Mastercard" + ], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -104,8 +100,14 @@ }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": ["{{baseUrl}}"], - "path": ["account", ":account_id", "connectors"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], "variable": [ { "key": "account_id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json index 07ffc4eedefc..872fac0738c4 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Create/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json index ef0b213739db..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/request.json @@ -7,9 +7,20 @@ } ], "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json index 5f4c58816d58..5e306df7a552 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json index c4271891fbff..6c28619e8566 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Refunds - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/refunds/:id", - "host": ["{{baseUrl}}"], - "path": ["refunds", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json index 9cbdbc8e22f8..ad281c58256a 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp Year)/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json index 6e9db26a339d..03e71d6c7eae 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid Exp month)/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json index 418f77270d94..2d0e17e60e8a 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(Invalid card number)/request.json @@ -75,8 +75,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json index 0b35b7a4e92b..bd5526d6f86b 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario1-Create payment with Invalid card details/Payments - Create(invalid CVV)/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json index f2bee31ef66f..90b19864ee18 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Confirm"] + "childrenOrder": [ + "Payments - Create", + "Payments - Confirm" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json index 98eb4829e4bd..cffc5ba2ed34 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/request.json @@ -43,8 +43,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json index e3b1e235042f..b28abd0c3090 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Create/request.json @@ -67,8 +67,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json index 6c934a9132cf..766a8330d6e8 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json index 5b606850fd2e..c4f2248a4f45 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json index 6df7c0a19439..cafd3a6808e5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Capture"] + "childrenOrder": [ + "Payments - Create", + "Payments - Capture" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json index 220b1a6723d5..4ac527d834af 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js", "event.prerequest.js"] + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json index 6c934a9132cf..766a8330d6e8 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Capture/request.json @@ -25,8 +25,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/capture", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "capture"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js index c48d8e2d054e..16c37817f0b1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario4-Capture the succeeded payment/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json index 5ff1a7cb4e8e..afba25ab6d6e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/.meta.json @@ -1,3 +1,6 @@ { - "childrenOrder": ["Payments - Create", "Payments - Cancel"] + "childrenOrder": [ + "Payments - Create", + "Payments - Cancel" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json index 3a1d8aa178f2..f64e37a125a2 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Cancel/request.json @@ -23,8 +23,14 @@ }, "url": { "raw": "{{baseUrl}}/payments/:id/cancel", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "cancel"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], "variable": [ { "key": "id", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js index c48d8e2d054e..16c37817f0b1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json index 09987daa71ec..06284e30aedc 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario5-Void the success_slash_failure payment/Payments - Create/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js index 2f020d7ff20d..4b3558a20cc5 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/event.test.js @@ -60,12 +60,12 @@ if (jsonData?.client_secret) { ); } -// Response body should have value "succeeded" for "status" +// Response body should have value "processing" for "status" if (jsonData?.status) { pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + "[POST]::/payments - Content check if value for 'status' matches 'processing'", function () { - pm.expect(jsonData.status).to.eql("succeeded"); + pm.expect(jsonData.status).to.eql("processing"); }, ); } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json index 144a35f773aa..47f08f9f3721 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Create/request.json @@ -76,8 +76,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json index 10de81bed082..326319e8fdf4 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json index 45feadeb3c38..758498447dd9 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Create/request.json @@ -77,8 +77,12 @@ }, "url": { "raw": "{{baseUrl}}/payments", - "host": ["{{baseUrl}}"], - "path": ["payments"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] }, "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json index 6cd4b7d96c52..b9ebc1be4aa3 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/request.json @@ -8,8 +8,13 @@ ], "url": { "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], "query": [ { "key": "force_sync", diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json index 0731450e6b25..2df9d47d936d 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/.event.meta.json @@ -1,3 +1,6 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json index 9fe125ce8ea4..d18aaf8befdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Refunds - Create/request.json @@ -31,8 +31,12 @@ }, "url": { "raw": "{{baseUrl}}/refunds", - "host": ["{{baseUrl}}"], - "path": ["refunds"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] }, "description": "To create a refund against an already processed payment" } diff --git a/postman/collection-dir/checkout/Health check/.meta.json b/postman/collection-dir/checkout/Health check/.meta.json index 579f9fd541ca..66ee7e50cab8 100644 --- a/postman/collection-dir/checkout/Health check/.meta.json +++ b/postman/collection-dir/checkout/Health check/.meta.json @@ -1,3 +1,5 @@ { - "childrenOrder": ["New Request"] + "childrenOrder": [ + "New Request" + ] } diff --git a/postman/collection-dir/checkout/Health check/New Request/.event.meta.json b/postman/collection-dir/checkout/Health check/New Request/.event.meta.json index 0731450e6b25..688c85746ef1 100644 --- a/postman/collection-dir/checkout/Health check/New Request/.event.meta.json +++ b/postman/collection-dir/checkout/Health check/New Request/.event.meta.json @@ -1,3 +1,5 @@ { - "eventOrder": ["event.test.js"] + "eventOrder": [ + "event.test.js" + ] } diff --git a/postman/collection-dir/checkout/Health check/New Request/request.json b/postman/collection-dir/checkout/Health check/New Request/request.json index e40e93961785..4cc8d4b1a966 100644 --- a/postman/collection-dir/checkout/Health check/New Request/request.json +++ b/postman/collection-dir/checkout/Health check/New Request/request.json @@ -10,7 +10,11 @@ ], "url": { "raw": "{{baseUrl}}/health", - "host": ["{{baseUrl}}"], - "path": ["health"] + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] } } diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index 5d3345e57544..3ca54dcdfeeb 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -1,13894 +1,13901 @@ { - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", - "}", - "", - "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", - "" - ], - "type": "text/javascript" - } - } - ], - "item": [ - { - "name": "Health check", - "item": [ - { - "name": "New Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Flow Testcases", - "item": [ - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ - { - "name": "Scenario11-Save card flow", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"checkout\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario12-Don't Pass CVV for save card flow and verify success payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario13-Pass Invalid CVV for save card flow and verify failed payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'cvv_invalid'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"cvv_invalid\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'cvv_invalid'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"cvv_invalid\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario14-Save card payment with manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"checkout\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// //Validate the amount", - "// pm.test(\"[POST]::/payments - connector\", function () {", - "// // pm.expect(jsonData.connector).to.eql(\"paypal\");", - "// pm.expect(jsonData.amount).to.eql(pm.request.amount);", - "// });", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Create payment with Manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Create Partial Capture payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Void the payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario8-Refund full payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9-Partial refund", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create-copy", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"1000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario10-Multiple Captures", - "item": [ - { - "name": "Successful Partial Capture and Refund", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture - 1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Capture - 2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Capture - 3", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - }, - { - "key": "expand_captures", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Retrieve After Partial Capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - }, - { - "key": "expand_captures", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Cancel After Partial Capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Response body should have value \"0\" for \"amount_capturable\"", - "if (jsonData?.amount){", - " pm.test(\"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\", function() {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " } )", - "} ", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - } - ] - }, - { - "name": "Refund After Partial Capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "3DS Payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - } - ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ - { - "name": "Scenario1-Create payment with Invalid card details", - "item": [ - { - "name": "Payments - Create(Invalid card number)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4644968546281686\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp month)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Invalid Expiry Month\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Month'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Month\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp Year)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Invalid Expiry Year\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(invalid CVV)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Invalid card_cvc length\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid card_cvc length'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid card_cvc length\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Confirming the payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"A payment token or payment method data is required\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"A payment token or payment method data is required\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Capture greater amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"amount_to_capture is greater than amount\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"amount_to_capture is greater than amount\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Capture the succeeded payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the success_slash_failure payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Refund exceeds amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"The refund amount exceeds the amount captured\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"The refund amount exceeds the amount captured\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Refund for unsuccessful payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - } - ] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "info": { - "_postman_id": "0da8c3be-4466-413e-9e72-81b21533423e", - "name": "Checkout Collection", - "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "25737662" - }, - "variable": [ - { - "key": "baseUrl", - "value": "https://sandbox.hyperswitch.io", - "type": "string" - }, - { - "key": "admin_api_key", - "value": "", - "type": "string" - }, - { - "key": "api_key", - "value": "", - "type": "string" - }, - { - "key": "merchant_id", - "value": "" - }, - { - "key": "payment_id", - "value": "" - }, - { - "key": "customer_id", - "value": "" - }, - { - "key": "mandate_id", - "value": "" - }, - { - "key": "payment_method_id", - "value": "" - }, - { - "key": "refund_id", - "value": "" - }, - { - "key": "merchant_connector_id", - "value": "" - }, - { - "key": "client_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_api_key", - "value": "", - "type": "string" - }, - { - "key": "connector_key1", - "value": "", - "type": "string" - }, - { - "key": "connector_api_secret", - "value": "", - "type": "string" - }, - { - "key": "publishable_key", - "value": "", - "type": "string" - }, - { - "key": "api_key_id", - "value": "", - "type": "string" - }, - { - "key": "payment_token", - "value": "" - }, - { - "key": "gateway_merchant_id", - "value": "", - "type": "string" - }, - { - "key": "certificate", - "value": "", - "type": "string" - }, - { - "key": "certificate_keys", - "value": "", - "type": "string" - } - ] -} + "info": { + "_postman_id": "9ab8f157-6b4b-430a-9ca8-34931682f988", + "name": "Checkout Collection", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "25180378", + "_collection_link": "https://galactic-desert-365744.postman.co/workspace/postman-tests-for-all-connector~f274d40a-132c-47e3-8240-6793392ee4d1/collection/25180378-9ab8f157-6b4b-430a-9ca8-34931682f988?action=share&creator=25180378&source=collection_link" + }, + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"amount_to_capture\": 6540,\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4005519200000004\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"payment_id\": \"{{payment_id}}\",\n \"amount\": 6540,\n \"reason\": \"Customer returned product\",\n \"refund_type\": \"instant\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario11-Save card flow", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"checkout\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario12-Don't Pass CVV for save card flow and verify success payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "// Response body should have value \"checkout\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario13-Pass Invalid CVV for save card flow and verify failed payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'cvv_invalid'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"cvv_invalid\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'cvv_invalid'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"cvv_invalid\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario14-Save card payment with manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"checkout\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// //Validate the amount", + "// pm.test(\"[POST]::/payments - connector\", function () {", + "// // pm.expect(jsonData.connector).to.eql(\"paypal\");", + "// pm.expect(jsonData.amount).to.eql(pm.request.amount);", + "// });", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://google.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4485040371536584\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": false,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://google.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4005519200000004\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Create Partial Capture payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Void the payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario8-Refund full payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Partial refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario10-Multiple Captures", + "item": [ + { + "name": "Successful Partial Capture and Refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture - 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Capture - 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Capture - 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + }, + { + "key": "expand_captures", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Retrieve After Partial Capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 6000,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"manual_multiple\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4242424242424242\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n },\n \"routing\": {\n \"type\": \"single\",\n \"data\": \"checkout\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + }, + { + "key": "expand_captures", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Cancel After Partial Capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"0\" for \"amount_capturable\"", + "if (jsonData?.amount){", + " pm.test(\"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\", function() {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " } )", + "} ", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + } + ] + }, + { + "name": "Refund After Partial Capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "3DS Payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario1-Create payment with Invalid card details", + "item": [ + { + "name": "Payments - Create(Invalid card number)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4644968546281686\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp month)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Invalid Expiry Month\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Month'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Month\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp Year)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Invalid Expiry Year\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(invalid CVV)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Invalid card_cvc length\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid card_cvc length'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid card_cvc length\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Confirming the payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"A payment token or payment method data is required\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"A payment token or payment method data is required\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Capture greater amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"amount_to_capture is greater than amount\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"amount_to_capture is greater than amount\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Capture the succeeded payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the success_slash_failure payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Refund exceeds amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Refund amount exceeds the payment amount\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"Refund amount exceeds the payment amount\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Refund for unsuccessful payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "https://sandbox.hyperswitch.io", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file From 4ae6af4632bbef5d21c3cb28538dcc4a94a10789 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 15 Dec 2023 17:07:55 +0530 Subject: [PATCH 206/443] feat(connector): [CYBERSOURCE] Implement Google Pay (#3139) --- crates/router/src/configs/defaults.rs | 87 +++++ .../src/connector/cybersource/transformers.rs | 345 +++++++++++++----- 2 files changed, 349 insertions(+), 83 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 4abb878043fd..80e98a68f5d0 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4809,6 +4809,93 @@ impl Default for super::settings::RequiredFields { ), common: HashMap::new(), } + ), + ( + enums::Connector::Cybersource, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } ) ]), }, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index a4cea13e2184..a5b55f111b9e 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,12 +1,13 @@ use api_models::payments; +use base64::Engine; use common_utils::pii; use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self, AddressDetailsData, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, - RouterData, + self, AddressDetailsData, CardData, PaymentsAuthorizeRequestData, + PaymentsSetupMandateRequestData, RouterData, }, consts, core::errors, @@ -66,7 +67,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { let order_information = OrderInformationWithBill { amount_details: Amount { total_amount: "0".to_string(), - currency: item.request.currency.to_string(), + currency: item.request.currency, }, bill_to: Some(bill_to), }; @@ -90,6 +91,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { action_token_types, authorization_options, commerce_indicator: CybersourceCommerceIndicator::Internet, + payment_solution: None, }; let client_reference_information = ClientReferenceInformation { @@ -103,11 +105,12 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, security_code: ccard.card_cvc, + card_type: None, }); - PaymentInformation { + PaymentInformation::Cards(CardPaymentInformation { card, instrument_identifier: None, - } + }) } _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), @@ -140,6 +143,7 @@ pub struct ProcessingInformation { commerce_indicator: CybersourceCommerceIndicator, capture: Option, capture_options: Option, + payment_solution: Option, } #[derive(Debug, Serialize)] @@ -197,11 +201,30 @@ pub struct CaptureOptions { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct PaymentInformation { +pub struct CardPaymentInformation { card: CardDetails, instrument_identifier: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FluidData { + value: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentInformation { + fluid_data: FluidData, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymentInformation { + Cards(CardPaymentInformation), + GooglePay(GooglePayPaymentInformation), +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CybersoucreInstrumentIdentifier { id: String, @@ -221,6 +244,8 @@ pub struct Card { expiration_month: Secret, expiration_year: Secret, security_code: Secret, + #[serde(rename = "type")] + card_type: Option, } #[derive(Debug, Serialize)] @@ -253,7 +278,7 @@ pub struct OrderInformation { #[serde(rename_all = "camelCase")] pub struct Amount { total_amount: String, - currency: String, + currency: api_models::enums::Currency, } #[derive(Debug, Serialize)] @@ -263,6 +288,22 @@ pub struct AdditionalAmount { currency: String, } +#[derive(Debug, Serialize)] +pub enum PaymentSolution { + ApplePay, + GooglePay, +} + +impl From for String { + fn from(solution: PaymentSolution) -> Self { + let payment_solution = match solution { + PaymentSolution::ApplePay => "001", + PaymentSolution::GooglePay => "012", + }; + payment_solution.to_string() + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { @@ -276,6 +317,82 @@ pub struct BillTo { email: pii::Email, } +impl From<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + +impl + From<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + )> for ProcessingInformation +{ + fn from( + (item, solution): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Option, + ), + ) -> Self { + let (action_list, action_token_types, authorization_options) = + if item.router_data.request.setup_future_usage.is_some() { + ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }, + merchant_intitiated_transaction: None, + }), + ) + } else { + (None, None, None) + }; + Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + action_list, + action_token_types, + authorization_options, + capture_options: None, + commerce_indicator: CybersourceCommerceIndicator::Internet, + } + } +} + +impl + From<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to: Some(bill_to), + } + } +} + // for cybersource each item in Billing is mandatory fn build_bill_to( address_details: &payments::Address, @@ -297,85 +414,150 @@ fn build_bill_to( }) } -impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> - for CybersourcePaymentsRequest +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + )> for CybersourcePaymentsRequest { type Error = error_stack::Report; fn try_from( - item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + (item, ccard): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::Card, + ), ) -> Result { let email = item.router_data.request.get_email()?; let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); - let order_information = OrderInformationWithBill { - amount_details: Amount { - total_amount: item.amount.to_owned(), - currency: item.router_data.request.currency.to_string(), - }, - bill_to: Some(bill_to), + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, }; - let (action_list, action_token_types, authorization_options) = - if item.router_data.request.setup_future_usage.is_some() { - ( - Some(vec![CybersourceActionsList::TokenCreate]), - Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), - Some(CybersourceAuthorizationOptions { - initiator: CybersourcePaymentInitiator { - initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), - credential_stored_on_file: Some(true), - stored_credential_used: None, - }, - merchant_intitiated_transaction: None, - }), - ) - } else { - (None, None, None) - }; - let processing_information = ProcessingInformation { - capture: Some(matches!( - item.router_data.request.capture_method, - Some(enums::CaptureMethod::Automatic) | None - )), - capture_options: None, - action_list, - action_token_types, - authorization_options, - commerce_indicator: CybersourceCommerceIndicator::Internet, - }; + let instrument_identifier = + item.router_data + .request + .connector_mandate_id() + .map(|mandate_token_id| CybersoucreInstrumentIdentifier { + id: mandate_token_id, + }); - let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.connector_request_reference_id.clone()), + let card = if instrument_identifier.is_some() { + CardDetails::MandateCard(MandateCardDetails { + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + }) + } else { + CardDetails::PaymentCard(Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }) }; - let payment_information = match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ccard) => { - let instrument_identifier = - item.router_data - .request - .connector_mandate_id() - .map(|mandate_token_id| CybersoucreInstrumentIdentifier { - id: mandate_token_id, - }); - let card = if instrument_identifier.is_some() { - CardDetails::MandateCard(MandateCardDetails { - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - }) - } else { - CardDetails::PaymentCard(Card { - number: ccard.card_number, - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, - }) - }; - PaymentInformation { - card, - instrument_identifier, + + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card, + instrument_identifier, + }); + + let processing_information = ProcessingInformation::from((item, None)); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, google_pay_data): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + payments::GooglePayWalletData, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let payment_information = PaymentInformation::GooglePay(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE.encode(google_pay_data.tokenization_data.token), + ), + }, + }); + + let processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); + let client_reference_information = ClientReferenceInformation::from(item); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> + for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::GooglePay(google_pay_data) => { + Self::try_from((item, google_pay_data)) } - } + payments::WalletData::ApplePay(_) + | payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ) + .into()), + }, payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Wallet(_) | payments::PaymentMethodData::PayLater(_) | payments::PaymentMethodData::BankRedirect(_) | payments::PaymentMethodData::BankDebit(_) @@ -389,15 +571,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), - ))? + ) + .into()) } - }; - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - }) + } } } @@ -433,11 +610,12 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> authorization_options: None, capture: None, commerce_indicator: CybersourceCommerceIndicator::Internet, + payment_solution: None, }, order_information: OrderInformationWithBill { amount_details: Amount { total_amount: item.amount.clone(), - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, }, bill_to: None, }, @@ -469,6 +647,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout commerce_indicator: CybersourceCommerceIndicator::Internet, capture: None, capture_options: None, + payment_solution: None, }, order_information: OrderInformationIncrementalAuthorization { amount_details: AdditionalAmount { @@ -917,7 +1096,7 @@ impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for Cybers order_information: OrderInformation { amount_details: Amount { total_amount: item.amount.clone(), - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, }, }, }) From d47a7cc418b0f4bb609d99f4a463a14c39df46e4 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:27:05 +0530 Subject: [PATCH 207/443] fix(connector): [Cybersource] signature authentication in incremental_authorization flow (#3141) --- crates/router/src/connector/cybersource.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index f74ab55595dd..a2f14801a486 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -54,10 +54,17 @@ impl Cybersource { api_secret, } = auth; let is_post_method = matches!(http_method, services::Method::Post); - let digest_str = if is_post_method { "digest " } else { "" }; + let is_patch_method = matches!(http_method, services::Method::Patch); + let digest_str = if is_post_method || is_patch_method { + "digest " + } else { + "" + }; let headers = format!("host date (request-target) {digest_str}v-c-merchant-id"); let request_target = if is_post_method { format!("(request-target): post {resource}\ndigest: SHA-256={payload}\n") + } else if is_patch_method { + format!("(request-target): patch {resource}\ndigest: SHA-256={payload}\n") } else { format!("(request-target): get {resource}\n") }; @@ -854,6 +861,10 @@ impl self.build_headers(req, connectors) } + fn get_http_method(&self) -> services::Method { + services::Method::Patch + } + fn get_content_type(&self) -> &'static str { self.common_get_content_type() } From a78fed73babace05b4f668ef219909277045ba85 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Sat, 16 Dec 2023 20:39:05 +0530 Subject: [PATCH 208/443] chore(events): remove duplicate logs (#3148) --- crates/router/src/services/api.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index ea254ee4fabf..ecce3b9e7c63 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -279,15 +279,6 @@ where // If needed add an error stack as follows // connector_integration.build_request(req).attach_printable("Failed to build request"); logger::debug!(connector_request=?connector_request); - let masked_conn_req = connector_request.as_ref().map(|req| match &req.body { - Some(RequestContent::Json(payload)) - | Some(RequestContent::FormUrlEncoded(payload)) - | Some(RequestContent::Xml(payload)) => payload.masked_serialize().unwrap_or_default(), - Some(RequestContent::FormData(_)) => json!({"request_type": "FORM_DATA"}), - None => serde_json::Value::Null, - }); - logger::debug!(connector_request_body=?masked_conn_req); - logger::debug!(payment_id=?req.payment_id); let mut router_data = req.clone(); match call_connector_action { payments::CallConnectorAction::HandleResponse(res) => { From 62c0c47e99f154399687a32caf9999b365da60ae Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Sun, 17 Dec 2023 12:42:11 +0530 Subject: [PATCH 209/443] fix: [CYBERSOURCE] Fix Status Mapping (#3144) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/cybersource.rs | 75 +-- .../src/connector/cybersource/transformers.rs | 452 +++++++++++++----- 2 files changed, 359 insertions(+), 168 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index a2f14801a486..06be499fae4f 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -418,15 +418,11 @@ impl ConnectorIntegration CustomResult { - let response: cybersource::CybersourcePaymentsResponse = res + let response: cybersource::CybersourceRefundResponse = res .response - .parse_struct("Cybersource PaymentResponse") + .parse_struct("Cybersource RefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; 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, @@ -826,17 +805,15 @@ impl ConnectorIntegration CustomResult { - let response: cybersource::CybersourceTransactionResponse = res + let response: cybersource::CybersourceRsyncResponse = res .response - .parse_struct("Cybersource RefundsSyncResponse") + .parse_struct("Cybersource RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::ResponseRouterData { + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, - } - .try_into() - .change_context(errors::ConnectorError::ResponseHandlingFailed) + }) } fn get_error_response( &self, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index a5b55f111b9e..ef31b9770753 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ self, AddressDetailsData, CardData, PaymentsAuthorizeRequestData, - PaymentsSetupMandateRequestData, RouterData, + PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RouterData, }, consts, core::errors, @@ -15,6 +15,7 @@ use crate::{ self, api::{self, enums as api_enums}, storage::enums, + transformers::ForeignFrom, }, }; @@ -684,7 +685,8 @@ impl TryFrom<&types::ConnectorAuthType> for CybersourceAuthType { } } } -#[derive(Debug, Default, Clone, Deserialize)] + +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CybersourcePaymentStatus { Authorized, @@ -696,8 +698,6 @@ pub enum CybersourcePaymentStatus { Declined, AuthorizedPendingReview, Transmitted, - #[default] - Processing, } #[derive(Debug, Clone, Deserialize)] @@ -708,18 +708,30 @@ pub enum CybersourceIncrementalAuthorizationStatus { AuthorizedPendingReview, } -impl From for enums::AttemptStatus { - fn from(item: CybersourcePaymentStatus) -> Self { - match item { +impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { + fn foreign_from((status, capture): (CybersourcePaymentStatus, bool)) -> Self { + match status { CybersourcePaymentStatus::Authorized - | CybersourcePaymentStatus::AuthorizedPendingReview => Self::Authorized, + | CybersourcePaymentStatus::AuthorizedPendingReview => { + if capture { + // Because Cybersource will return Payment Status as Authorized even in AutoCapture Payment + Self::Charged + } else { + Self::Authorized + } + } + CybersourcePaymentStatus::Pending => { + if capture { + Self::Charged + } else { + Self::Pending + } + } CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => { Self::Charged } CybersourcePaymentStatus::Voided | CybersourcePaymentStatus::Reversed => Self::Voided, CybersourcePaymentStatus::Failed | CybersourcePaymentStatus::Declined => Self::Failure, - CybersourcePaymentStatus::Processing => Self::Authorizing, - CybersourcePaymentStatus::Pending => Self::Pending, } } } @@ -746,16 +758,29 @@ impl From for enums::RefundStatus { } } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourcePaymentsResponse { + ClientReferenceInformation(CybersourceClientReferenceResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + +#[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CybersourcePaymentsResponse { +pub struct CybersourceClientReferenceResponse { id: String, status: CybersourcePaymentStatus, - error_information: Option, - client_reference_information: Option, + client_reference_information: ClientReferenceInformation, token_information: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceErrorInformationResponse { + id: String, + error_information: CybersourceErrorInformation, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsIncrementalAuthorizationResponse { @@ -787,69 +812,201 @@ pub struct CybersourceTokenInformation { #[derive(Debug, Clone, Deserialize)] pub struct CybersourceErrorInformation { - reason: String, - message: String, + reason: Option, + message: Option, } impl - TryFrom<( + From<( + &CybersourceErrorInformationResponse, types::ResponseRouterData, - bool, )> for types::RouterData { - type Error = error_stack::Report; - fn try_from( - data: ( + fn from( + (error_response, item): ( + &CybersourceErrorInformationResponse, types::ResponseRouterData< F, CybersourcePaymentsResponse, T, types::PaymentsResponseData, >, - bool, ), + ) -> Self { + Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response + .error_information + .message + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: error_response.error_information.reason.clone(), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + ..item.data + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, ) -> Result { - let item = data.0; - let is_capture = data.1; - let mandate_reference = - item.response - .token_information - .map(|token_info| types::MandateReference { - connector_mandate_id: Some(token_info.instrument_identifier.id), - payment_method_id: None, - }); - let status = get_payment_status(is_capture, item.response.status.into()); - Ok(Self { - status, - response: match item.response.error_information { - Some(error) => Err(types::ErrorResponse { - code: consts::NO_ERROR_CODE.to_string(), - message: error.message, - reason: Some(error.reason), - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(item.response.id), - }), - _ => Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.id.clone(), - ), - redirection_data: None, - mandate_reference, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: item - .response + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status, + item.data.request.is_auto_capture()?, + )); + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + let mandate_reference = + info_response + .token_information + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); + let connector_response_reference_id = Some( + info_response .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(item.response.id)), - incremental_authorization_allowed: Some( - status == enums::AttemptStatus::Authorized, - ), - }), - }, - ..item.data - }) + .code + .unwrap_or(info_response.id.clone()), + ); + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed, + }), + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item))) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCaptureData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from((info_response.status, true)); + let connector_response_reference_id = Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ); + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item))) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::PaymentsCancelData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from((info_response.status, false)); + let connector_response_reference_id = Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ); + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item))) + } + } } } @@ -879,7 +1036,7 @@ impl connector_mandate_id: Some(token_info.instrument_identifier.id), payment_method_id: None, }); - let mut mandate_status: enums::AttemptStatus = item.response.status.into(); + let mut mandate_status = enums::AttemptStatus::foreign_from((item.response.status, false)); if matches!(mandate_status, enums::AttemptStatus::Authorized) { //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. mandate_status = enums::AttemptStatus::Charged @@ -889,8 +1046,10 @@ impl response: match item.response.error_information { Some(error) => Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error.message, - reason: Some(error.reason), + message: error + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: error.reason, status_code: item.http_code, attempt_status: None, connector_transaction_id: Some(item.response.id), @@ -947,8 +1106,8 @@ impl Some(error) => Ok( types::PaymentsResponseData::IncrementalAuthorizationResponse { status: common_enums::AuthorizationStatus::Failure, - error_code: Some(error.reason), - error_message: Some(error.message), + error_code: error.reason, + error_message: error.message, connector_authorization_id: None, }, ), @@ -966,9 +1125,16 @@ impl } } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceTransactionResponse { + ApplicationInformation(CybersourceApplicationInfoResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CybersourceTransactionResponse { +pub struct CybersourceApplicationInfoResponse { id: String, application_information: ApplicationInformation, client_reference_information: Option, @@ -980,61 +1146,68 @@ pub struct ApplicationInformation { status: CybersourcePaymentStatus, } -fn get_payment_status(is_capture: bool, status: enums::AttemptStatus) -> enums::AttemptStatus { - let is_authorized = matches!(status, enums::AttemptStatus::Authorized); - let is_pending = matches!(status, enums::AttemptStatus::Pending); - if is_capture && (is_authorized || is_pending) { - return enums::AttemptStatus::Charged; - } - status -} - -impl - TryFrom<( +impl + TryFrom< types::ResponseRouterData< F, CybersourceTransactionResponse, - T, + types::PaymentsSyncData, types::PaymentsResponseData, >, - bool, - )> for types::RouterData + > for types::RouterData { type Error = error_stack::Report; fn try_from( - data: ( - types::ResponseRouterData< - F, - CybersourceTransactionResponse, - T, - types::PaymentsResponseData, - >, - bool, - ), + item: types::ResponseRouterData< + F, + CybersourceTransactionResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, ) -> Result { - let item = data.0; - let is_capture = data.1; - let status = get_payment_status( - is_capture, - item.response.application_information.status.into(), - ); - Ok(Self { - status, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: item - .response - .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(item.response.id)), - incremental_authorization_allowed: Some(status == enums::AttemptStatus::Authorized), + match item.response { + CybersourceTransactionResponse::ApplicationInformation(app_response) => { + let status = enums::AttemptStatus::foreign_from(( + app_response.application_information.status, + item.data.request.is_auto_capture()?, + )); + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + app_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + incremental_authorization_allowed, + connector_response_reference_id: app_response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(app_response.id)), + }), + ..item.data + }) + } + CybersourceTransactionResponse::ErrorInformation(error_response) => Ok(Self { + status: item.data.status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + error_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(error_response.id), + incremental_authorization_allowed: None, + }), + ..item.data }), - ..item.data - }) + } } } @@ -1103,30 +1276,71 @@ impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for Cybers } } -impl TryFrom> +impl From for enums::RefundStatus { + fn from(item: CybersourceRefundStatus) -> Self { + match item { + CybersourceRefundStatus::Succeeded | CybersourceRefundStatus::Transmitted => { + Self::Success + } + CybersourceRefundStatus::Failed | CybersourceRefundStatus::Voided => Self::Failure, + CybersourceRefundStatus::Pending => Self::Pending, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceRefundStatus { + Succeeded, + Transmitted, + Failed, + Pending, + Voided, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceRefundResponse { + id: String, + status: CybersourceRefundStatus, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { - let refund_status = enums::RefundStatus::from(item.response.status); Ok(Self { response: Ok(types::RefundsResponseData { connector_refund_id: item.response.id, - refund_status, + refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data }) } } -impl TryFrom> +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RsyncApplicationInformation { + status: CybersourceRefundStatus, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceRsyncResponse { + id: String, + application_information: RsyncApplicationInformation, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { From 107c66fec331376aa8c9f1e710e1503793fde119 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Sun, 17 Dec 2023 13:25:53 +0530 Subject: [PATCH 210/443] feat(connector): [PlaceToPay] Implement Cards for PlaceToPay (#3117) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 2 +- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/router/src/connector/placetopay.rs | 177 ++++++-- .../src/connector/placetopay/transformers.rs | 421 ++++++++++++++---- crates/router/src/connector/utils.rs | 6 + loadtest/config/development.toml | 2 +- 7 files changed, 482 insertions(+), 130 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index fb1c12a7d7ef..7b4380ba2db0 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -216,7 +216,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" -placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/config/development.toml b/config/development.toml index 9646a0a0456d..b6a0f9f99cd9 100644 --- a/config/development.toml +++ b/config/development.toml @@ -203,7 +203,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" -placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 13d405131bda..eab1ea5408c0 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -138,7 +138,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" -placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/crates/router/src/connector/placetopay.rs b/crates/router/src/connector/placetopay.rs index 1b80bcf7f79c..82998e97a06a 100644 --- a/crates/router/src/connector/placetopay.rs +++ b/crates/router/src/connector/placetopay.rs @@ -4,21 +4,21 @@ use std::fmt::Debug; use common_utils::request::RequestContent; use error_stack::{IntoReport, ResultExt}; -use masking::ExposeInterface; use transformers as placetopay; use crate::{ configs::settings, + connector::utils::{self}, core::errors::{self, CustomResult}, headers, services::{ self, - request::{self, Mask}, + request::{self}, ConnectorIntegration, ConnectorValidation, }, types::{ self, - api::{self, ConnectorCommon, ConnectorCommonExt}, + api::{self, enums, ConnectorCommon, ConnectorCommonExt}, ErrorResponse, Response, }, utils::BytesExt, @@ -86,18 +86,6 @@ impl ConnectorCommon for Placetopay { connectors.placetopay.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = placetopay::PlacetopayAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, @@ -109,16 +97,32 @@ impl ConnectorCommon for Placetopay { Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.status.reason.to_owned(), + message: response.status.message.to_owned(), + reason: Some(response.status.message), attempt_status: None, connector_transaction_id: None, }) } } -impl ConnectorValidation for Placetopay {} +impl ConnectorValidation for Placetopay { + fn validate_capture_method( + &self, + capture_method: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::Automatic + | enums::CaptureMethod::ManualMultiple + | enums::CaptureMethod::Scheduled => Err(utils::construct_not_supported_error_report( + capture_method, + self.id(), + )), + } + } +} impl ConnectorIntegration for Placetopay @@ -157,9 +161,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/process", self.base_url(connectors))) } fn get_request_body( @@ -206,7 +210,7 @@ impl ConnectorIntegration CustomResult { let response: placetopay::PlacetopayPaymentsResponse = res .response - .parse_struct("Placetopay PaymentsAuthorizeResponse") + .parse_struct("Placetopay PlacetopayPaymentsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -241,9 +245,18 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/query", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayPsyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -253,10 +266,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() - .method(services::Method::Get) + .method(services::Method::Post) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .build(), )) } @@ -303,17 +319,18 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/transaction", self.base_url(connectors))) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCaptureRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let req_obj = placetopay::PlacetopayNextActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -363,6 +380,75 @@ impl ConnectorIntegration for Placetopay { + 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 { + Ok(format!("{}/transaction", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayNextActionRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: placetopay::PlacetopayPaymentsResponse = res + .response + .parse_struct("Placetopay PaymentCancelResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -383,9 +469,9 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/transaction", self.base_url(connectors))) } fn get_request_body( @@ -393,13 +479,7 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_router_data = placetopay::PlacetopayRouterData::try_from(( - &self.get_currency_unit(), - req.request.currency, - req.request.refund_amount, - req, - ))?; - let req_obj = placetopay::PlacetopayRefundRequest::try_from(&connector_router_data)?; + let req_obj = placetopay::PlacetopayRefundRequest::try_from(req)?; Ok(RequestContent::Json(Box::new(req_obj))) } @@ -427,9 +507,9 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: placetopay::RefundResponse = res + let response: placetopay::PlacetopayRefundResponse = res .response - .parse_struct("placetopay RefundResponse") + .parse_struct("placetopay PlacetopayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -464,9 +544,18 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/query", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = placetopay::PlacetopayRsyncRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(req_obj))) } fn build_request( @@ -492,9 +581,9 @@ impl ConnectorIntegration CustomResult { - let response: placetopay::RefundResponse = res + let response: placetopay::PlacetopayRefundResponse = res .response - .parse_struct("placetopay RefundSyncResponse") + .parse_struct("placetopay PlacetopayRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, diff --git a/crates/router/src/connector/placetopay/transformers.rs b/crates/router/src/connector/placetopay/transformers.rs index e947c6830c35..7fef8b8954ba 100644 --- a/crates/router/src/connector/placetopay/transformers.rs +++ b/crates/router/src/connector/placetopay/transformers.rs @@ -1,14 +1,23 @@ -use masking::Secret; +use api_models::payments; +use common_utils::date_time; +use diesel_models::enums; +use error_stack::{IntoReport, ResultExt}; +use masking::{PeekInterface, Secret}; +use ring::digest; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{ + self, BrowserInformationData, CardData, PaymentsAuthorizeRequestData, + PaymentsSyncRequestData, RouterData, + }, + consts, core::errors, - types::{self, api, storage::enums}, + types::{self, api, storage::enums as storage_enums}, }; pub struct PlacetopayRouterData { - pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: i64, pub router_data: T, } @@ -36,19 +45,65 @@ impl } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct PlacetopayPaymentsRequest { - amount: i64, + auth: PlacetopayAuth, + payment: PlacetopayPayment, + instrument: PlacetopayInstrument, + ip_address: Secret, + user_agent: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum PlacetopayAuthorizeAction { + Checkin, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayAuthType { + login: Secret, + tran_key: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayAuth { + login: Secret, + tran_key: Secret, + nonce: String, + seed: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayPayment { + reference: String, + description: String, + amount: PlacetopayAmount, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayAmount { + currency: storage_enums::Currency, + total: i64, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayInstrument { card: PlacetopayCard, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] pub struct PlacetopayCard { number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + expiration: Secret, + cvv: Secret, } impl TryFrom<&PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>> @@ -58,65 +113,132 @@ impl TryFrom<&PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { + let browser_info = item.router_data.request.get_browser_info()?; + let ip_address = browser_info.get_ip_address()?; + let user_agent = browser_info.get_user_agent()?; + let auth = PlacetopayAuth::try_from(&item.router_data.connector_auth_type)?; + let payment = PlacetopayPayment { + reference: item.router_data.connector_request_reference_id.clone(), + description: item.router_data.get_description()?, + amount: PlacetopayAmount { + currency: item.router_data.request.currency, + total: item.amount, + }, + }; match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(req_card) => { + payments::PaymentMethodData::Card(req_card) => { let card = PlacetopayCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, + number: req_card.card_number.clone(), + expiration: req_card + .clone() + .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + cvv: req_card.card_cvc.clone(), }; Ok(Self { - amount: item.amount.to_owned(), - card, + ip_address, + user_agent, + auth, + payment, + instrument: PlacetopayInstrument { + card: card.to_owned(), + }, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Placetopay"), + ) + .into()) + } } } } -// Auth Struct -pub struct PlacetopayAuthType { - pub(super) api_key: Secret, +impl TryFrom<&types::ConnectorAuthType> for PlacetopayAuth { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + let placetopay_auth = PlacetopayAuthType::try_from(auth_type)?; + let nonce_bytes = utils::generate_random_bytes(16); + let now = error_stack::IntoReport::into_report(date_time::date_as_yyyymmddthhmmssmmmz()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let seed = format!("{}+00:00", now.split_at(now.len() - 5).0); + let mut context = digest::Context::new(&digest::SHA256); + context.update(&nonce_bytes); + context.update(seed.as_bytes()); + context.update(placetopay_auth.tran_key.peek().as_bytes()); + let encoded_digest = base64::Engine::encode(&consts::BASE64_ENGINE, context.finish()); + let nonce = base64::Engine::encode(&consts::BASE64_ENGINE, &nonce_bytes); + Ok(Self { + login: placetopay_auth.login, + tran_key: encoded_digest.into(), + nonce, + seed, + }) + } } impl TryFrom<&types::ConnectorAuthType> for PlacetopayAuthType { 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_owned(), - }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + if let types::ConnectorAuthType::BodyKey { api_key, key1 } = auth_type { + Ok(Self { + login: api_key.to_owned(), + tran_key: key1.to_owned(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? } } } -// PaymentsResponse -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum PlacetopayPaymentStatus { - Succeeded, + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PlacetopayStatus { + Ok, Failed, - #[default] - Processing, + Approved, + Rejected, + Pending, + PendingValidation, + PendingProcess, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayStatusResponse { + status: PlacetopayStatus, } -impl From for enums::AttemptStatus { - fn from(item: PlacetopayPaymentStatus) -> Self { +impl From for enums::AttemptStatus { + fn from(item: PlacetopayStatus) -> Self { match item { - PlacetopayPaymentStatus::Succeeded => Self::Charged, - PlacetopayPaymentStatus::Failed => Self::Failure, - PlacetopayPaymentStatus::Processing => Self::Authorizing, + PlacetopayStatus::Approved | PlacetopayStatus::Ok => Self::Authorized, + PlacetopayStatus::Failed | PlacetopayStatus::Rejected => Self::Failure, + PlacetopayStatus::Pending + | PlacetopayStatus::PendingValidation + | PlacetopayStatus::PendingProcess => Self::Authorizing, } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct PlacetopayPaymentsResponse { - status: PlacetopayPaymentStatus, - id: String, + status: PlacetopayStatusResponse, + internal_reference: u64, } impl @@ -134,9 +256,11 @@ impl >, ) -> Result { Ok(Self { - status: enums::AttemptStatus::from(item.response.status), + status: enums::AttemptStatus::from(item.response.status.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.internal_reference.to_string(), + ), redirection_data: None, mandate_reference: None, connector_metadata: None, @@ -148,61 +272,76 @@ impl }) } } + // REFUND : // Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct PlacetopayRefundRequest { - pub amount: i64, + auth: PlacetopayAuth, + internal_reference: u64, + action: PlacetopayNextAction, } -impl TryFrom<&PlacetopayRouterData<&types::RefundsRouterData>> for PlacetopayRefundRequest { +impl TryFrom<&types::RefundsRouterData> for PlacetopayRefundRequest { type Error = error_stack::Report; - fn try_from( - item: &PlacetopayRouterData<&types::RefundsRouterData>, - ) -> Result { + fn try_from(item: &types::RefundsRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Refund; + Ok(Self { - amount: item.amount.to_owned(), + auth, + internal_reference, + action, }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: PlacetopayRefundStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, + PlacetopayRefundStatus::Refunded => Self::Success, + PlacetopayRefundStatus::Failed | PlacetopayRefundStatus::Rejected => Self::Failure, + PlacetopayRefundStatus::Pending | PlacetopayRefundStatus::PendingProcess => { + Self::Pending + } } } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRefundResponse { + status: PlacetopayRefundStatus, + internal_reference: u64, } -impl TryFrom> +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PlacetopayRefundStatus { + Refunded, + Rejected, + Failed, + Pending, + PendingProcess, +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + connector_refund_id: item.response.internal_reference.to_string(), refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data @@ -210,16 +349,40 @@ impl TryFrom> } } -impl TryFrom> +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayRsyncRequest { + auth: PlacetopayAuth, + internal_reference: u64, +} + +impl TryFrom<&types::RefundsRouterData> for PlacetopayRsyncRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + auth, + internal_reference, + }) + } +} + +impl TryFrom> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + item: types::RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), + connector_refund_id: item.response.internal_reference.to_string(), refund_status: enums::RefundStatus::from(item.response.status), }), ..item.data @@ -227,10 +390,104 @@ impl TryFrom> } } -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct PlacetopayErrorResponse { - pub status_code: u16, - pub code: String, + pub status: PlacetopayError, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayError { + pub status: PlacetopayErrorStatus, pub message: String, - pub reason: Option, + pub reason: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PlacetopayErrorStatus { + Failed, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayPsyncRequest { + auth: PlacetopayAuth, + internal_reference: u64, +} + +impl TryFrom<&types::PaymentsSyncRouterData> for PlacetopayPsyncRequest { + type Error = error_stack::Report; + + fn try_from(item: &types::PaymentsSyncRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .get_connector_transaction_id()? + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + auth, + internal_reference, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlacetopayNextActionRequest { + auth: PlacetopayAuth, + internal_reference: u64, + action: PlacetopayNextAction, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum PlacetopayNextAction { + Refund, + Void, + Process, + Checkout, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for PlacetopayNextActionRequest { + type Error = error_stack::Report; + + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Checkout; + Ok(Self { + auth, + internal_reference, + action, + }) + } +} + +impl TryFrom<&types::PaymentsCancelRouterData> for PlacetopayNextActionRequest { + type Error = error_stack::Report; + + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let auth = PlacetopayAuth::try_from(&item.connector_auth_type)?; + let internal_reference = item + .request + .connector_transaction_id + .parse::() + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let action = PlacetopayNextAction::Void; + Ok(Self { + auth, + internal_reference, + action, + }) + } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index d06caf3ae202..9a538a7207e9 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1630,6 +1630,12 @@ pub fn is_manual_capture(capture_method: Option) -> bool { || capture_method == Some(enums::CaptureMethod::ManualMultiple) } +pub fn generate_random_bytes(length: usize) -> Vec { + // returns random bytes of length n + let mut rng = rand::thread_rng(); + (0..length).map(|_| rand::Rng::gen(&mut rng)).collect() +} + pub fn validate_currency( request_currency: types::storage::enums::Currency, merchant_config_currency: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 883c243c6c44..43eb06820dc5 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -111,7 +111,7 @@ payeezy.base_url = "https://api-cert.payeezy.com/" payme.base_url = "https://sandbox.payme.io/" paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" -placetopay.base_url = "https://api-co-dev.placetopay.ws/gateway" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" From 5f53d84a8b92f8aab67d09666b45362b287809ff Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Sun, 17 Dec 2023 14:10:50 +0530 Subject: [PATCH 211/443] feat(connector): [CYBERSOURCE] Implement Apple Pay (#3149) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/configs/defaults.rs | 87 +++++++++++++++++++ .../src/connector/cybersource/transformers.rs | 81 ++++++++++++++++- 2 files changed, 165 insertions(+), 3 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 80e98a68f5d0..83a34b87dd0b 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4707,6 +4707,93 @@ impl Default for super::settings::RequiredFields { ), common: HashMap::new(), } + ), + ( + enums::Connector::Cybersource, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } ) ]), }, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index ef31b9770753..21dbed5e5e4e 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self, AddressDetailsData, CardData, PaymentsAuthorizeRequestData, + self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RouterData, }, consts, @@ -16,6 +16,7 @@ use crate::{ api::{self, enums as api_enums}, storage::enums, transformers::ForeignFrom, + ApplePayPredecryptData, }, }; @@ -207,6 +208,22 @@ pub struct CardPaymentInformation { instrument_identifier: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenizedCard { + number: Secret, + expiration_month: Secret, + expiration_year: Secret, + cryptogram: Secret, + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayPaymentInformation { + tokenized_card: TokenizedCard, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FluidData { @@ -224,6 +241,7 @@ pub struct GooglePayPaymentInformation { pub enum PaymentInformation { Cards(CardPaymentInformation), GooglePay(GooglePayPaymentInformation), + ApplePay(ApplePayPaymentInformation), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -295,6 +313,12 @@ pub enum PaymentSolution { GooglePay, } +#[derive(Debug, Serialize)] +pub enum TransactionType { + #[serde(rename = "1")] + ApplePay, +} + impl From for String { fn from(solution: PaymentSolution) -> Self { let payment_solution = match solution { @@ -478,6 +502,47 @@ impl } } +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, apple_pay_data): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + Box, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + let processing_information = + ProcessingInformation::from((item, Some(PaymentSolution::ApplePay))); + let client_reference_information = ClientReferenceInformation::from(item); + let expiration_month = apple_pay_data.get_expiry_month()?; + let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; + + let payment_information = PaymentInformation::ApplePay(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: apple_pay_data.application_primary_account_number, + cryptogram: apple_pay_data.payment_data.online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) + } +} + impl TryFrom<( &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, @@ -526,11 +591,21 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> match item.router_data.request.payment_method_data.clone() { payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::ApplePay(_) => { + let payment_method_token = item.router_data.get_payment_method_token()?; + match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + } + } payments::WalletData::GooglePay(google_pay_data) => { Self::try_from((item, google_pay_data)) } - payments::WalletData::ApplePay(_) - | payments::WalletData::AliPayQr(_) + payments::WalletData::AliPayQr(_) | payments::WalletData::AliPayRedirect(_) | payments::WalletData::AliPayHkRedirect(_) | payments::WalletData::MomoRedirect(_) From 7df45235b1b55c3e4f1205169fb512d2aadc98ac Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Sun, 17 Dec 2023 14:38:37 +0530 Subject: [PATCH 212/443] feat(connector): [NMI] Implement 3DS for Cards (#3143) Co-authored-by: Arjun Karthik Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- crates/router/src/connector/nmi.rs | 169 ++++++++++ .../router/src/connector/nmi/transformers.rs | 307 +++++++++++++++++- crates/router/src/core/payments.rs | 7 + crates/router/src/core/payments/flows.rs | 2 - crates/router/src/services/api.rs | 82 ++++- 5 files changed, 556 insertions(+), 11 deletions(-) diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index 83a62b130104..aecb103194f1 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -187,6 +187,90 @@ impl } } +impl api::PaymentsPreProcessing for Nmi {} + +impl + ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for Nmi +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + 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::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}api/transact.php", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = nmi::NmiVaultRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req = Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( + self, req, connectors, + )?) + .build(), + ); + Ok(req) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: types::Response, + ) -> CustomResult { + let response: nmi::NmiVaultResponse = serde_urlencoded::from_bytes(&res.response) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Nmi { @@ -265,6 +349,91 @@ impl ConnectorIntegration for Nmi +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}api/transact.php", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = nmi::NmiRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = nmi::NmiCompleteRequest::try_from(&connector_router_data)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { + let response: nmi::NmiCompleteResponse = serde_urlencoded::from_bytes(&res.response) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Nmi { diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 35c0e102020e..b0403d11e3e4 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -1,12 +1,13 @@ use cards::CardNumber; -use common_utils::ext_traits::XmlExt; +use common_utils::{errors::CustomResult, ext_traits::XmlExt}; use error_stack::{IntoReport, Report, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsAuthorizeRequestData}, + connector::utils::{self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData}, core::errors, + services, types::{self, api, storage::enums, transformers::ForeignFrom, ConnectorAuthType}, }; @@ -25,17 +26,22 @@ pub enum TransactionType { pub struct NmiAuthType { pub(super) api_key: Secret, + pub(super) public_key: Option>, } impl TryFrom<&ConnectorAuthType> for NmiAuthType { type Error = Error; fn try_from(auth_type: &ConnectorAuthType) -> Result { - if let types::ConnectorAuthType::HeaderKey { api_key } = auth_type { - Ok(Self { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { api_key: api_key.to_owned(), - }) - } else { - Err(errors::ConnectorError::FailedToObtainAuthType.into()) + public_key: None, + }), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + api_key: api_key.to_owned(), + public_key: Some(key1.to_owned()), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } @@ -71,6 +77,291 @@ impl } } +#[derive(Debug, Serialize)] +pub struct NmiVaultRequest { + security_key: Secret, + ccnumber: CardNumber, + ccexp: Secret, + customer_vault: CustomerAction, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum CustomerAction { + AddCustomer, + UpdateCustomer, +} + +impl TryFrom<&types::PaymentsPreProcessingRouterData> for NmiVaultRequest { + type Error = Error; + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { + let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; + let (ccnumber, ccexp) = get_card_details(item.request.payment_method_data.clone())?; + + Ok(Self { + security_key: auth_type.api_key, + ccnumber, + ccexp, + customer_vault: CustomerAction::AddCustomer, + }) + } +} + +fn get_card_details( + payment_method_data: Option, +) -> CustomResult<(CardNumber, Secret), errors::ConnectorError> { + match payment_method_data { + Some(api::PaymentMethodData::Card(ref card_details)) => Ok(( + card_details.card_number.clone(), + utils::CardData::get_card_expiry_month_year_2_digit_with_delimiter( + card_details, + "".to_string(), + ), + )), + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Nmi"), + )) + .into_report(), + } +} + +#[derive(Debug, Deserialize)] +pub struct NmiVaultResponse { + pub response: Response, + pub responsetext: String, + pub customer_vault_id: String, + pub response_code: String, +} + +impl + TryFrom< + types::ResponseRouterData< + api::PreProcessing, + NmiVaultResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::PaymentsPreProcessingRouterData +{ + type Error = Error; + fn try_from( + item: types::ResponseRouterData< + api::PreProcessing, + NmiVaultResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + ) -> Result { + let auth_type: NmiAuthType = (&item.data.connector_auth_type).try_into()?; + let amount_data = + item.data + .request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?; + let currency_data = + item.data + .request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?; + let (response, status) = match item.response.response { + Response::Approved => ( + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::Nmi { + amount: utils::to_currency_base_unit_asf64( + amount_data, + currency_data.to_owned(), + )? + .to_string(), + currency: currency_data, + customer_vault_id: item.response.customer_vault_id, + public_key: auth_type.public_key.ok_or( + errors::ConnectorError::InvalidConnectorConfig { + config: "public_key", + }, + )?, + }), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + enums::AttemptStatus::AuthenticationPending, + ), + Response::Declined | Response::Error => ( + Err(types::ErrorResponse { + code: item.response.response_code, + message: item.response.responsetext, + reason: None, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }), + enums::AttemptStatus::Failure, + ), + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} + +#[derive(Debug, Serialize)] +pub struct NmiCompleteRequest { + #[serde(rename = "type")] + transaction_type: TransactionType, + security_key: Secret, + cardholder_auth: CardHolderAuthType, + cavv: String, + xid: String, + three_ds_version: ThreeDsVersion, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CardHolderAuthType { + Verified, + Attempted, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum ThreeDsVersion { + #[serde(rename = "2.0.0")] + VersionTwo, + #[serde(rename = "2.2.0")] + VersionTwoPointTwo, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NmiRedirectResponseData { + cavv: String, + xid: String, + card_holder_auth: CardHolderAuthType, + three_ds_version: ThreeDsVersion, +} + +impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for NmiCompleteRequest { + type Error = Error; + fn try_from( + item: &NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let transaction_type = match item.router_data.request.is_auto_capture()? { + true => TransactionType::Sale, + false => TransactionType::Auth, + }; + let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?; + let payload_data = item + .router_data + .request + .get_redirect_response_payload()? + .expose(); + + let three_ds_data: NmiRedirectResponseData = serde_json::from_value(payload_data) + .into_report() + .change_context(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "three_ds_data", + })?; + + Ok(Self { + transaction_type, + security_key: auth_type.api_key, + cardholder_auth: three_ds_data.card_holder_auth, + cavv: three_ds_data.cavv, + xid: three_ds_data.xid, + three_ds_version: three_ds_data.three_ds_version, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct NmiCompleteResponse { + pub response: Response, + pub responsetext: String, + pub authcode: Option, + pub transactionid: String, + pub avsresponse: Option, + pub cvvresponse: Option, + pub orderid: String, + pub response_code: String, +} + +impl + TryFrom< + types::ResponseRouterData< + api::CompleteAuthorize, + NmiCompleteResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::PaymentsCompleteAuthorizeRouterData +{ + type Error = Error; + fn try_from( + item: types::ResponseRouterData< + api::CompleteAuthorize, + NmiCompleteResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + let (response, status) = match item.response.response { + Response::Approved => ( + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transactionid, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }), + if let Some(diesel_models::enums::CaptureMethod::Automatic) = + item.data.request.capture_method + { + enums::AttemptStatus::CaptureInitiated + } else { + enums::AttemptStatus::Authorizing + }, + ), + Response::Declined | Response::Error => ( + Err(types::ErrorResponse::foreign_from(( + item.response, + item.http_code, + ))), + enums::AttemptStatus::Failure, + ), + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} + +impl ForeignFrom<(NmiCompleteResponse, u16)> for types::ErrorResponse { + fn foreign_from((response, http_code): (NmiCompleteResponse, u16)) -> Self { + Self { + code: response.response_code, + message: response.responsetext, + reason: None, + status_code: http_code, + attempt_status: None, + connector_transaction_id: None, + } + } +} + #[derive(Debug, Serialize)] pub struct NmiPaymentsRequest { #[serde(rename = "type")] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 44c46732529e..bfd747640d3f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1479,6 +1479,13 @@ where let is_error_in_response = router_data.response.is_err(); // If is_error_in_response is true, should_continue_payment should be false, we should throw the error (router_data, !is_error_in_response) + } else if connector.connector_name == router_types::Connector::Nmi + && !matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + && router_data.auth_type == storage_enums::AuthenticationType::ThreeDs + { + router_data = router_data.preprocessing_steps(state, connector).await?; + + (router_data, false) } else { (router_data, should_continue_payment) } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 394051f1432b..ec8e13cff509 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -165,7 +165,6 @@ default_imp_for_complete_authorize!( connector::Klarna, connector::Multisafepay, connector::Nexinets, - connector::Nmi, connector::Noon, connector::Opayo, connector::Opennode, @@ -886,7 +885,6 @@ default_imp_for_pre_processing_steps!( connector::Mollie, connector::Multisafepay, connector::Nexinets, - connector::Nmi, connector::Noon, connector::Nuvei, connector::Opayo, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index ecce3b9e7c63..a9fa574800ea 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -12,6 +12,7 @@ use std::{ use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError}; use api_models::enums::CaptureMethod; pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient}; +use common_enums::Currency; pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; use common_utils::{ consts::X_HS_LATENCY, @@ -19,7 +20,7 @@ use common_utils::{ request::RequestContent, }; use error_stack::{report, IntoReport, Report, ResultExt}; -use masking::PeekInterface; +use masking::{PeekInterface, Secret}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; use serde_json::json; @@ -772,6 +773,12 @@ pub enum RedirectForm { card_token: String, bin: String, }, + Nmi { + amount: String, + currency: Currency, + public_key: Secret, + customer_vault_id: String, + }, } impl From<(url::Url, Method)> for RedirectForm { @@ -1495,6 +1502,79 @@ pub fn build_redirection_form( ))) }} } + RedirectForm::Nmi { + amount, + currency, + public_key, + customer_vault_id, + } => { + let public_key_val = public_key.peek(); + maud::html! { + (maud::DOCTYPE) + head { + (PreEscaped(r#""#)) + } + (PreEscaped(format!("" + ))) + } + } } } From d40de4c8b51010a9e6a3164196702a20c2ab3563 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 14:11:35 +0000 Subject: [PATCH 213/443] test(postman): update postman collection files --- .../checkout.postman_collection.json | 27800 ++++++++-------- 1 file changed, 13900 insertions(+), 13900 deletions(-) diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index 3ca54dcdfeeb..a46cae1df50e 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -1,13901 +1,13901 @@ { - "info": { - "_postman_id": "9ab8f157-6b4b-430a-9ca8-34931682f988", - "name": "Checkout Collection", - "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "25180378", - "_collection_link": "https://galactic-desert-365744.postman.co/workspace/postman-tests-for-all-connector~f274d40a-132c-47e3-8240-6793392ee4d1/collection/25180378-9ab8f157-6b4b-430a-9ca8-34931682f988?action=share&creator=25180378&source=collection_link" - }, - "item": [ - { - "name": "Health check", - "item": [ - { - "name": "New Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Flow Testcases", - "item": [ - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"amount_to_capture\": 6540,\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4005519200000004\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"payment_id\": \"{{payment_id}}\",\n \"amount\": 6540,\n \"reason\": \"Customer returned product\",\n \"refund_type\": \"instant\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ - { - "name": "Scenario11-Save card flow", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"checkout\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario12-Don't Pass CVV for save card flow and verify success payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "// Response body should have value \"checkout\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario13-Pass Invalid CVV for save card flow and verify failed payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'cvv_invalid'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"cvv_invalid\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'cvv_invalid'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"cvv_invalid\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario14-Save card payment with manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"checkout\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// //Validate the amount", - "// pm.test(\"[POST]::/payments - connector\", function () {", - "// // pm.expect(jsonData.connector).to.eql(\"paypal\");", - "// pm.expect(jsonData.amount).to.eql(pm.request.amount);", - "// });", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://google.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4485040371536584\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"amount\": 6540,\n \"currency\": \"USD\",\n \"confirm\": false,\n \"capture_method\": \"automatic\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://google.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4005519200000004\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Create payment with Manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Create Partial Capture payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Void the payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario8-Refund full payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9-Partial refund", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create-copy", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"1000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario10-Multiple Captures", - "item": [ - { - "name": "Successful Partial Capture and Refund", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture - 1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Capture - 2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Capture - 3", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - }, - { - "key": "expand_captures", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Retrieve After Partial Capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"amount\": 6000,\n \"currency\": \"USD\",\n \"confirm\": true,\n \"capture_method\": \"manual_multiple\",\n \"capture_on\": \"2022-09-10T10:11:12Z\",\n \"customer_id\": \"StripeCustomer\",\n \"email\": \"guest@example.com\",\n \"name\": \"John Doe\",\n \"phone\": \"999999999\",\n \"phone_country_code\": \"+1\",\n \"description\": \"Its my first payment request\",\n \"authentication_type\": \"no_three_ds\",\n \"return_url\": \"https://duck.com\",\n \"payment_method\": \"card\",\n \"payment_method_data\": {\n \"card\": {\n \"card_number\": \"4242424242424242\",\n \"card_exp_month\": \"10\",\n \"card_exp_year\": \"25\",\n \"card_holder_name\": \"joseph Doe\",\n \"card_cvc\": \"123\"\n }\n },\n \"billing\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"shipping\": {\n \"address\": {\n \"line1\": \"1467\",\n \"line2\": \"Harrison Street\",\n \"line3\": \"Harrison Street\",\n \"city\": \"San Fransico\",\n \"state\": \"California\",\n \"zip\": \"94122\",\n \"country\": \"US\",\n \"first_name\": \"PiX\"\n }\n },\n \"statement_descriptor_name\": \"joseph\",\n \"statement_descriptor_suffix\": \"JS\",\n \"metadata\": {\n \"udf1\": \"value1\",\n \"new_customer\": \"true\",\n \"login_date\": \"2019-09-10T10:11:12Z\"\n },\n \"routing\": {\n \"type\": \"single\",\n \"data\": \"checkout\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - }, - { - "key": "expand_captures", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Cancel After Partial Capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", - " },", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancellation succeeded\" for \"payment status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Response body should have value \"0\" for \"amount_capturable\"", - "if (jsonData?.amount){", - " pm.test(\"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\", function() {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " } )", - "} ", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - } - ] - }, - { - "name": "Refund After Partial Capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "3DS Payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - } - ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ - { - "name": "Scenario1-Create payment with Invalid card details", - "item": [ - { - "name": "Payments - Create(Invalid card number)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4644968546281686\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp month)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Invalid Expiry Month\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Month'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Month\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp Year)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Invalid Expiry Year\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(invalid CVV)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Invalid card_cvc length\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid card_cvc length'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid card_cvc length\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Confirming the payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"A payment token or payment method data is required\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"A payment token or payment method data is required\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_secret\":\"{{client_secret}}\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Capture greater amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(null);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"amount_to_capture is greater than amount\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"amount_to_capture is greater than amount\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Capture the succeeded payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the success_slash_failure payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Refund exceeds amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"null\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"checkout\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"Refund amount exceeds the payment amount\" for \"message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'Refund amount exceeds the payment amount'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"Refund amount exceeds the payment amount\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Refund for unsuccessful payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "setTimeout(function(){", - " console.log(\"Sleeping for 3 seconds before next request.\");", - "}, 3000);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - } - ] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", - "}", - "", - "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", - "" - ] - } - } - ], - "variable": [ - { - "key": "baseUrl", - "value": "https://sandbox.hyperswitch.io", - "type": "string" - }, - { - "key": "admin_api_key", - "value": "", - "type": "string" - }, - { - "key": "api_key", - "value": "", - "type": "string" - }, - { - "key": "merchant_id", - "value": "" - }, - { - "key": "payment_id", - "value": "" - }, - { - "key": "customer_id", - "value": "" - }, - { - "key": "mandate_id", - "value": "" - }, - { - "key": "payment_method_id", - "value": "" - }, - { - "key": "refund_id", - "value": "" - }, - { - "key": "merchant_connector_id", - "value": "" - }, - { - "key": "client_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_api_key", - "value": "", - "type": "string" - }, - { - "key": "connector_key1", - "value": "", - "type": "string" - }, - { - "key": "connector_api_secret", - "value": "", - "type": "string" - }, - { - "key": "publishable_key", - "value": "", - "type": "string" - }, - { - "key": "api_key_id", - "value": "", - "type": "string" - }, - { - "key": "payment_token", - "value": "" - }, - { - "key": "gateway_merchant_id", - "value": "", - "type": "string" - }, - { - "key": "certificate", - "value": "", - "type": "string" - }, - { - "key": "certificate_keys", - "value": "", - "type": "string" - } - ] -} \ No newline at end of file + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario11-Save card flow", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"checkout\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario12-Don't Pass CVV for save card flow and verify success payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "// Response body should have value \"checkout\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario13-Pass Invalid CVV for save card flow and verify failed payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'cvv_invalid'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"cvv_invalid\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'cvv_invalid'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"cvv_invalid\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario14-Save card payment with manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"checkout\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'checkout'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// //Validate the amount", + "// pm.test(\"[POST]::/payments - connector\", function () {", + "// // pm.expect(jsonData.connector).to.eql(\"paypal\");", + "// pm.expect(jsonData.amount).to.eql(pm.request.amount);", + "// });", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4485040371536584\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Create Partial Capture payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Void the payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario8-Refund full payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Partial refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario10-Multiple Captures", + "item": [ + { + "name": "Successful Partial Capture and Refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture - 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Capture - 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Capture - 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + }, + { + "key": "expand_captures", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Retrieve After Partial Capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true&expand_captures=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + }, + { + "key": "expand_captures", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Cancel After Partial Capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured_and_capturable'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured_and_capturable\");", + " },", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancellation succeeded\" for \"payment status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"0\" for \"amount_capturable\"", + "if (jsonData?.amount){", + " pm.test(\"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 0'\", function() {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " } )", + "} ", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + } + ] + }, + { + "name": "Refund After Partial Capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":2000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":2000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "3DS Payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'jsonData.status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6000,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual_multiple\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"checkout\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario1-Create payment with Invalid card details", + "item": [ + { + "name": "Payments - Create(Invalid card number)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4644968546281686\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp month)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Invalid Expiry Month\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Month'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Month\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"13\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp Year)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Invalid Expiry Year\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(invalid CVV)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"Invalid card_cvc length\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid card_cvc length'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid card_cvc length\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Confirming the payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"A payment token or payment method data is required\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"A payment token or payment method data is required\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and existing payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Capture greater amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(null);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"amount_to_capture is greater than amount\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"amount_to_capture is greater than amount\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Capture the succeeded payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the success_slash_failure payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Refund exceeds amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"null\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_received' matches 'null'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 6540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4005519200000004\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"checkout\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"The refund amount exceeds the amount captured\" for \"message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"The refund amount exceeds the amount captured\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Refund for unsuccessful payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "9ab8f157-6b4b-430a-9ca8-34931682f988", + "name": "Checkout Collection", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "25180378", + "_collection_link": "https://galactic-desert-365744.postman.co/workspace/postman-tests-for-all-connector~f274d40a-132c-47e3-8240-6793392ee4d1/collection/25180378-9ab8f157-6b4b-430a-9ca8-34931682f988?action=share&creator=25180378&source=collection_link" + }, + "variable": [ + { + "key": "baseUrl", + "value": "https://sandbox.hyperswitch.io", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + } + ] +} From 3e7d683cd0240493960608b24be56c9a531fc1dd Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 14:11:36 +0000 Subject: [PATCH 214/443] chore(version): v1.102.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad610bbafde5..6aa1846f3b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.102.0 (2023-12-17) + +### Features + +- **connector:** + - [CYBERSOURCE] Implement Google Pay ([#3139](https://github.com/juspay/hyperswitch/pull/3139)) ([`4ae6af4`](https://github.com/juspay/hyperswitch/commit/4ae6af4632bbef5d21c3cb28538dcc4a94a10789)) + - [PlaceToPay] Implement Cards for PlaceToPay ([#3117](https://github.com/juspay/hyperswitch/pull/3117)) ([`107c66f`](https://github.com/juspay/hyperswitch/commit/107c66fec331376aa8c9f1e710e1503793fde119)) + - [CYBERSOURCE] Implement Apple Pay ([#3149](https://github.com/juspay/hyperswitch/pull/3149)) ([`5f53d84`](https://github.com/juspay/hyperswitch/commit/5f53d84a8b92f8aab67d09666b45362b287809ff)) + - [NMI] Implement 3DS for Cards ([#3143](https://github.com/juspay/hyperswitch/pull/3143)) ([`7df4523`](https://github.com/juspay/hyperswitch/commit/7df45235b1b55c3e4f1205169fb512d2aadc98ac)) + +### Bug Fixes + +- **connector:** + - [Checkout] Fix status mapping for checkout ([#3073](https://github.com/juspay/hyperswitch/pull/3073)) ([`5b2c329`](https://github.com/juspay/hyperswitch/commit/5b2c3291d4fbe3c4154c187b4e915dc3365e761a)) + - [Cybersource] signature authentication in incremental_authorization flow ([#3141](https://github.com/juspay/hyperswitch/pull/3141)) ([`d47a7cc`](https://github.com/juspay/hyperswitch/commit/d47a7cc418b0f4bb609d99f4a463a14c39df46e4)) +- [CYBERSOURCE] Fix Status Mapping ([#3144](https://github.com/juspay/hyperswitch/pull/3144)) ([`62c0c47`](https://github.com/juspay/hyperswitch/commit/62c0c47e99f154399687a32caf9999b365da60ae)) + +### Testing + +- **postman:** Update postman collection files ([`d40de4c`](https://github.com/juspay/hyperswitch/commit/d40de4c8b51010a9e6a3164196702a20c2ab3563)) + +### Miscellaneous Tasks + +- **deps:** Bump zerocopy from 0.7.26 to 0.7.31 ([#3136](https://github.com/juspay/hyperswitch/pull/3136)) ([`d8de3c2`](https://github.com/juspay/hyperswitch/commit/d8de3c285c90103da93f0f3fd0241924dabd256f)) +- **events:** Remove duplicate logs ([#3148](https://github.com/juspay/hyperswitch/pull/3148)) ([`a78fed7`](https://github.com/juspay/hyperswitch/commit/a78fed73babace05b4f668ef219909277045ba85)) + +**Full Changelog:** [`v1.101.0...v1.102.0`](https://github.com/juspay/hyperswitch/compare/v1.101.0...v1.102.0) + +- - - + + ## 1.101.0 (2023-12-14) ### Features From cc12e8a2435e5e47eeec77c620c747b156a3e16b Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Sun, 17 Dec 2023 09:37:42 -0800 Subject: [PATCH 215/443] refactor(router): [ACI] change payment error message from not supported to not implemented error (#2837) --- crates/router/src/connector/aci/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 53639f268c86..470c6705e1f0 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -412,10 +412,9 @@ impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPayment | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.router_data.payment_method), - connector: "Aci", - })?, + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Aci"), + ))?, } } } From 41b5a82bafa9b0392bb43ed268fefc5187b48636 Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Sun, 17 Dec 2023 09:38:09 -0800 Subject: [PATCH 216/443] refactor(connector): [Helcim] change error message from not supported to not implemented (#2850) --- crates/router/src/connector/helcim/transformers.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index a27a562ddc27..823096d66482 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -173,10 +173,9 @@ impl TryFrom<&types::SetupMandateRouterData> for HelcimVerifyRequest { | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::GiftCard(_) | api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.request.payment_method_data), - connector: "Helcim", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Helcim"), + ))? } } } @@ -274,10 +273,9 @@ impl TryFrom<&HelcimRouterData<&types::PaymentsAuthorizeRouterData>> for HelcimP | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::GiftCard(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.router_data.request.payment_method_data), - connector: "Helcim", - })?, + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Helcim"), + ))?, } } } From 3fc0e2d8195948d50f735df5192ae0f8431b432b Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Sun, 17 Dec 2023 09:38:43 -0800 Subject: [PATCH 217/443] refactor(connector): [Forte] change error message from not supported to not implemented (#2847) --- .../src/connector/forte/transformers.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index 411457fab671..ca7a66a9ffa7 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -54,10 +54,9 @@ impl TryFrom for ForteCardType { utils::CardIssuer::Visa => Ok(Self::Visa), utils::CardIssuer::DinersClub => Ok(Self::DinersClub), utils::CardIssuer::JCB => Ok(Self::Jcb), - _ => Err(errors::ConnectorError::NotSupported { - message: issuer.to_string(), - connector: "Forte", - } + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Forte"), + ) .into()), } } @@ -67,10 +66,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { if item.request.currency != enums::Currency::USD { - Err(errors::ConnectorError::NotSupported { - message: item.request.currency.to_string(), - connector: "Forte", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Forte"), + ))? } match item.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { @@ -117,10 +115,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::GiftCard(_) | api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Forte", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Forte"), + ))? } } } From 2d895be9856d17cd923665568aa9b6e54fc1a305 Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Sun, 17 Dec 2023 09:39:08 -0800 Subject: [PATCH 218/443] refactor(connector): [Cryptopay] change error message from not supported to not implemented (#2846) --- crates/router/src/connector/cryptopay/transformers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 3af604c786b8..4102945b201e 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -82,10 +82,9 @@ impl TryFrom<&CryptopayRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::GiftCard(_) | api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "CryptoPay", - }) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("CryptoPay"), + )) } }?; Ok(cryptopay_request) From 8e484ddab8d3f4463299c7f7e8ce75b8dd628599 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:59:00 +0530 Subject: [PATCH 219/443] fix(connector): [BOA/CYBERSOURCE] Update error handling (#3156) --- crates/router/src/connector/bankofamerica.rs | 75 ++++++++++------- .../connector/bankofamerica/transformers.rs | 37 ++++----- crates/router/src/connector/cybersource.rs | 74 ++++++++++------- .../src/connector/cybersource/transformers.rs | 83 +++++++++---------- 4 files changed, 146 insertions(+), 123 deletions(-) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index cac3bacc211d..b13bb299c2df 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -203,37 +203,50 @@ impl ConnectorCommon for Bankofamerica { } else { consts::NO_ERROR_MESSAGE }; - - let (code, message) = match response.error_information { - Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), - None => ( - response - .reason - .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { - reason.to_string() - }), - response - .message - .map_or(error_message.to_string(), |message| message), - ), - }; - let connector_reason = match response.details { - Some(details) => details - .iter() - .map(|det| format!("{} : {}", det.field, det.reason)) - .collect::>() - .join(", "), - None => message.clone(), - }; - - Ok(ErrorResponse { - status_code: res.status_code, - code, - message, - reason: Some(connector_reason), - attempt_status: None, - connector_transaction_id: None, - }) + match response { + transformers::BankOfAmericaErrorResponse::StandardError(response) => { + let (code, message) = match response.error_information { + Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), + None => ( + response + .reason + .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { + reason.to_string() + }), + response + .message + .map_or(error_message.to_string(), |message| message), + ), + }; + let connector_reason = match response.details { + Some(details) => details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", "), + None => message.clone(), + }; + + Ok(ErrorResponse { + status_code: res.status_code, + code, + message, + reason: Some(connector_reason), + attempt_status: None, + connector_transaction_id: None, + }) + } + transformers::BankOfAmericaErrorResponse::AuthenticationError(response) => { + Ok(ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response.response.rmsg.clone(), + reason: Some(response.response.rmsg), + attempt_status: None, + connector_transaction_id: None, + }) + } + } } } diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index bbec9022835c..e7c0c7a579b4 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1016,32 +1016,24 @@ impl TryFrom, pub status: Option, pub message: Option, - pub reason: Option, + pub reason: Option, pub details: Option>, } -#[derive(Debug, Deserialize, strum::Display)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum Reason { - MissingField, - InvalidData, - DuplicateRequest, - InvalidCard, - AuthAlreadyReversed, - CardTypeNotAccepted, - InvalidMerchantConfiguration, - ProcessorUnavailable, - InvalidAmount, - InvalidCardType, - InvalidPaymentId, - NotSupported, - SystemError, - ServerTimeout, - ServiceTimeout, +#[derive(Debug, Deserialize)] +pub struct BankOfAmericaAuthenticationErrorResponse { + pub response: AuthenticationErrorInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaErrorResponse { + StandardError(BankOfAmericaStandardErrorResponse), + AuthenticationError(BankOfAmericaAuthenticationErrorResponse), } #[derive(Debug, Deserialize, Clone)] @@ -1056,3 +1048,8 @@ pub struct ErrorInformation { pub message: String, pub reason: String, } + +#[derive(Debug, Default, Deserialize)] +pub struct AuthenticationErrorInformation { + pub rmsg: String, +} diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 06be499fae4f..1347fbfc93a2 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -109,44 +109,60 @@ impl ConnectorCommon for Cybersource { &self, res: types::Response, ) -> CustomResult { - let response: cybersource::ErrorResponse = res + let response: cybersource::CybersourceErrorResponse = res .response .parse_struct("Cybersource ErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let details = response.details.unwrap_or_default(); - let connector_reason = details - .iter() - .map(|det| format!("{} : {}", det.field, det.reason)) - .collect::>() - .join(", "); let error_message = if res.status_code == 401 { consts::CONNECTOR_UNAUTHORIZED_ERROR } else { consts::NO_ERROR_MESSAGE }; - - let (code, message) = match response.error_information { - Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), - None => ( - response - .reason - .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { - reason.to_string() - }), - response - .message - .map_or(error_message.to_string(), |message| message), - ), - }; - Ok(types::ErrorResponse { - status_code: res.status_code, - code, - message, - reason: Some(connector_reason), - attempt_status: None, - connector_transaction_id: None, - }) + match response { + transformers::CybersourceErrorResponse::StandardError(response) => { + let (code, message) = match response.error_information { + Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), + None => ( + response + .reason + .map_or(consts::NO_ERROR_CODE.to_string(), |reason| { + reason.to_string() + }), + response + .message + .map_or(error_message.to_string(), |message| message), + ), + }; + let connector_reason = match response.details { + Some(details) => details + .iter() + .map(|det| format!("{} : {}", det.field, det.reason)) + .collect::>() + .join(", "), + None => message.clone(), + }; + + Ok(types::ErrorResponse { + status_code: res.status_code, + code, + message, + reason: Some(connector_reason), + attempt_status: None, + connector_transaction_id: None, + }) + } + transformers::CybersourceErrorResponse::AuthenticationError(response) => { + Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response.response.rmsg.clone(), + reason: Some(response.response.rmsg), + attempt_status: None, + connector_transaction_id: None, + }) + } + } } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 21dbed5e5e4e..f99c90989d0c 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1286,49 +1286,6 @@ impl } } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ErrorResponse { - pub error_information: Option, - pub status: Option, - pub message: Option, - pub reason: Option, - pub details: Option>, -} - -#[derive(Debug, Deserialize, strum::Display)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum Reason { - MissingField, - InvalidData, - DuplicateRequest, - InvalidCard, - AuthAlreadyReversed, - CardTypeNotAccepted, - InvalidMerchantConfiguration, - ProcessorUnavailable, - InvalidAmount, - InvalidCardType, - InvalidPaymentId, - NotSupported, - SystemError, - ServerTimeout, - ServiceTimeout, -} - -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Details { - pub field: String, - pub reason: String, -} - -#[derive(Debug, Deserialize)] -pub struct ErrorInformation { - pub message: String, - pub reason: String, -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { @@ -1428,3 +1385,43 @@ impl TryFrom, + pub status: Option, + pub message: Option, + pub reason: Option, + pub details: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct CybersourceAuthenticationErrorResponse { + pub response: AuthenticationErrorInformation, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceErrorResponse { + StandardError(CybersourceStandardErrorResponse), + AuthenticationError(CybersourceAuthenticationErrorResponse), +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Details { + pub field: String, + pub reason: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ErrorInformation { + pub message: String, + pub reason: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct AuthenticationErrorInformation { + pub rmsg: String, +} From e3589e641c8a0b3b690b82f09a61d512db2d9932 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:40:26 +0530 Subject: [PATCH 220/443] refactor(users): rename `user_roles` and `dashboard_metadata` columns (#3135) Co-authored-by: Apoorv Dixit --- crates/diesel_models/src/enums.rs | 9 ++++--- crates/diesel_models/src/schema.rs | 8 +++--- crates/diesel_models/src/user_role.rs | 12 ++++----- crates/router/src/core/user.rs | 2 +- crates/router/src/db/user_role.rs | 2 +- crates/router/src/routes/api_keys.rs | 11 ++++++-- crates/router/src/types/domain/user.rs | 4 +-- .../down.sql | 4 +++ .../up.sql | 4 +++ .../down.sql | 3 +++ .../up.sql | 25 +++++++++++++++++++ 11 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 migrations/2023-12-14-060824_user_roles_user_status_column/down.sql create mode 100644 migrations/2023-12-14-060824_user_roles_user_status_column/up.sql create mode 100644 migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql create mode 100644 migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 17837d2ce5c7..792e8ffc8bb3 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -4,7 +4,8 @@ pub mod diesel_exports { DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbDashboardMetadata as DashboardMetadata, DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, DbFraudCheckStatus as FraudCheckStatus, DbFraudCheckType as FraudCheckType, @@ -16,7 +17,7 @@ pub mod diesel_exports { DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, - DbRoutingAlgorithmKind as RoutingAlgorithmKind, + DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbUserStatus as UserStatus, }; } pub use common_enums::*; @@ -418,7 +419,7 @@ pub enum FraudCheckLastStep { strum::EnumString, frunk::LabelledGeneric, )] -#[diesel_enum(storage_type = "text")] +#[diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum UserStatus { @@ -439,7 +440,7 @@ pub enum UserStatus { strum::EnumString, frunk::LabelledGeneric, )] -#[router_derive::diesel_enum(storage_type = "text")] +#[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum DashboardMetadata { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 0d4ab83d8232..f4a0437c6ccb 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -195,8 +195,7 @@ diesel::table! { merchant_id -> Varchar, #[max_length = 64] org_id -> Varchar, - #[max_length = 64] - data_key -> Varchar, + data_key -> DashboardMetadata, data_value -> Json, #[max_length = 64] created_by -> Varchar, @@ -978,14 +977,13 @@ diesel::table! { role_id -> Varchar, #[max_length = 64] org_id -> Varchar, - #[max_length = 64] - status -> Varchar, + status -> UserStatus, #[max_length = 64] created_by -> Varchar, #[max_length = 64] last_modified_by -> Varchar, created_at -> Timestamp, - last_modified_at -> Timestamp, + last_modified -> Timestamp, } } diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs index 467584ac59db..3c32092fa917 100644 --- a/crates/diesel_models/src/user_role.rs +++ b/crates/diesel_models/src/user_role.rs @@ -15,7 +15,7 @@ pub struct UserRole { pub created_by: String, pub last_modified_by: String, pub created_at: PrimitiveDateTime, - pub last_modified_at: PrimitiveDateTime, + pub last_modified: PrimitiveDateTime, } #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -29,7 +29,7 @@ pub struct UserRoleNew { pub created_by: String, pub last_modified_by: String, pub created_at: PrimitiveDateTime, - pub last_modified_at: PrimitiveDateTime, + pub last_modified: PrimitiveDateTime, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -38,7 +38,7 @@ pub struct UserRoleUpdateInternal { role_id: Option, status: Option, last_modified_by: Option, - last_modified_at: PrimitiveDateTime, + last_modified: PrimitiveDateTime, } pub enum UserRoleUpdate { @@ -54,7 +54,7 @@ pub enum UserRoleUpdate { impl From for UserRoleUpdateInternal { fn from(value: UserRoleUpdate) -> Self { - let last_modified_at = common_utils::date_time::now(); + let last_modified = common_utils::date_time::now(); match value { UserRoleUpdate::UpdateRole { role_id, @@ -63,14 +63,14 @@ impl From for UserRoleUpdateInternal { role_id: Some(role_id), last_modified_by: Some(modified_by), status: None, - last_modified_at, + last_modified, }, UserRoleUpdate::UpdateStatus { status, modified_by, } => Self { status: Some(status), - last_modified_at, + last_modified, last_modified_by: Some(modified_by), role_id: None, }, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index a9f46a3885f8..6401e1ea6ca7 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -381,7 +381,7 @@ pub async fn invite_user( created_by: user_from_token.user_id.clone(), last_modified_by: user_from_token.user_id, created_at: now, - last_modified_at: now, + last_modified: now, }) .await .map_err(|e| { diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index 37e38e8afca7..bf84ae134ea7 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -123,7 +123,7 @@ impl UserRoleInterface for MockDb { status: user_role.status, created_by: user_role.created_by, created_at: user_role.created_at, - last_modified_at: user_role.last_modified_at, + last_modified: user_role.last_modified, last_modified_by: user_role.last_modified_by, org_id: user_role.org_id, }; diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 5b4c047b1466..9293d6e11431 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -140,7 +140,7 @@ pub async fn api_key_update( let (merchant_id, key_id) = path.into_inner(); let mut payload = json_payload.into_inner(); payload.key_id = key_id; - payload.merchant_id = merchant_id; + payload.merchant_id = merchant_id.clone(); api::server_wrap( flow, @@ -148,7 +148,14 @@ pub async fn api_key_update( &req, payload, |state, _, payload| api_keys::update_api_key(state, payload), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id, + required_permission: Permission::ApiKeyWrite, + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 9bc27cba2b1d..a595afa4a27c 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -556,7 +556,7 @@ impl NewUser { user_id, role_id, created_at: now, - last_modified_at: now, + last_modified: now, org_id: self .get_new_merchant() .get_new_organization() @@ -829,7 +829,7 @@ impl TryFrom for user_api::UserDetails { role_id, status, role_name, - last_modified_at: user_and_role.1.last_modified_at, + last_modified_at: user_and_role.0.last_modified_at, }) } } diff --git a/migrations/2023-12-14-060824_user_roles_user_status_column/down.sql b/migrations/2023-12-14-060824_user_roles_user_status_column/down.sql new file mode 100644 index 000000000000..a246675a7d00 --- /dev/null +++ b/migrations/2023-12-14-060824_user_roles_user_status_column/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_roles RENAME COLUMN last_modified TO last_modified_at; +ALTER TABLE user_roles ALTER COLUMN status TYPE VARCHAR(64) USING (status::text); +DROP TYPE IF EXISTS "UserStatus"; diff --git a/migrations/2023-12-14-060824_user_roles_user_status_column/up.sql b/migrations/2023-12-14-060824_user_roles_user_status_column/up.sql new file mode 100644 index 000000000000..4d0245fcc3a1 --- /dev/null +++ b/migrations/2023-12-14-060824_user_roles_user_status_column/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE user_roles RENAME COLUMN last_modified_at TO last_modified; +CREATE TYPE "UserStatus" AS ENUM ('active', 'invitation_sent'); +ALTER TABLE user_roles ALTER COLUMN status TYPE "UserStatus" USING (status::"UserStatus"); diff --git a/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql new file mode 100644 index 000000000000..bd2a8e1060a5 --- /dev/null +++ b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE dashboard_metadata ALTER COLUMN data_key TYPE VARCHAR(64); +DROP TYPE IF EXISTS "DashboardMetadata"; diff --git a/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql new file mode 100644 index 000000000000..130ebe5b4a63 --- /dev/null +++ b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql @@ -0,0 +1,25 @@ +-- Your SQL goes here +CREATE TYPE "DashboardMetadata" AS ENUM ( + 'production_agreement', + 'setup_processor', + 'configure_endpoint', + 'setup_complete', + 'first_processor_connected', + 'second_processor_connected', + 'configured_routing', + 'test_payment', + 'integration_method', + 'stripe_connected', + 'paypal_connected', + 'sp_routing_configured', + 'sp_test_payment', + 'download_woocom', + 'configure_woocom', + 'setup_woocom_webhook', + 'is_multiple_configuration', + 'configuration_type', + 'feedback', + 'prodintent' +); + +ALTER TABLE dashboard_metadata ALTER COLUMN data_key TYPE "DashboardMetadata" USING (data_key::"DashboardMetadata"); \ No newline at end of file From 8db3361d80f674a28a3916830a4b0c1c2b89776a Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Mon, 18 Dec 2023 19:45:45 +0530 Subject: [PATCH 221/443] fix: change prodintent name in dashboard metadata (#3161) --- .../2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql index 130ebe5b4a63..653bee36866d 100644 --- a/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql +++ b/migrations/2023-12-14-101348_alter_dashboard_metadata_key_type/up.sql @@ -19,7 +19,7 @@ CREATE TYPE "DashboardMetadata" AS ENUM ( 'is_multiple_configuration', 'configuration_type', 'feedback', - 'prodintent' + 'prod_intent' ); ALTER TABLE dashboard_metadata ALTER COLUMN data_key TYPE "DashboardMetadata" USING (data_key::"DashboardMetadata"); \ No newline at end of file From 30fe9d19e4955035a370f8f9ce37963cdb76c68a Mon Sep 17 00:00:00 2001 From: Shanks Date: Mon, 18 Dec 2023 21:31:06 +0530 Subject: [PATCH 222/443] fix(euclid_wasm): add function to retrieve keys for 3ds and surcharge decision manager (#3160) Co-authored-by: hrithikeshvm --- crates/euclid_wasm/src/lib.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index 78c7677fe75c..134016191f53 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -6,7 +6,10 @@ use std::{ str::FromStr, }; -use api_models::{admin as admin_api, routing::ConnectorSelection}; +use api_models::{ + admin as admin_api, conditional_configs::ConditionalConfigs, routing::ConnectorSelection, + surcharge_decision_configs::SurchargeDecisionConfigs, +}; use common_enums::RoutableConnectors; use currency_conversion::{ conversion::convert as convert_currency, types as currency_conversion_types, @@ -20,7 +23,7 @@ use euclid::{ }, frontend::{ ast, - dir::{self, enums as dir_enums}, + dir::{self, enums as dir_enums, EuclidDirFilter}, }, }; use once_cell::sync::OnceCell; @@ -205,6 +208,18 @@ pub fn get_key_type(key: &str) -> Result { Ok(key_str) } +#[wasm_bindgen(js_name = getThreeDsKeys)] +pub fn get_three_ds_keys() -> JsResult { + let keys = ::ALLOWED; + Ok(serde_wasm_bindgen::to_value(keys)?) +} + +#[wasm_bindgen(js_name= getSurchargeKeys)] +pub fn get_surcharge_keys() -> JsResult { + let keys = ::ALLOWED; + Ok(serde_wasm_bindgen::to_value(keys)?) +} + #[wasm_bindgen(js_name=parseToString)] pub fn parser(val: String) -> String { ron_parser::my_parse(val) From 0fa61a9dd194c5b3688f8f68b056c263d92327d0 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Mon, 18 Dec 2023 21:32:31 +0530 Subject: [PATCH 223/443] fix(payment_link): added amount conversion to base unit based on currency (#3162) --- crates/api_models/src/payments.rs | 18 ++++++++- .../router/src/compatibility/stripe/errors.rs | 6 ++- .../src/core/errors/api_error_response.rs | 2 + crates/router/src/core/errors/transformers.rs | 3 ++ crates/router/src/core/payment_link.rs | 40 ++++++++++++++----- 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 717a908b1f0d..5efebb14f819 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3355,7 +3355,7 @@ pub struct PaymentLinkInitiateRequest { #[derive(Debug, serde::Serialize)] pub struct PaymentLinkDetails { - pub amount: i64, + pub amount: String, pub currency: api_enums::Currency, pub pub_key: String, pub client_secret: String, @@ -3365,7 +3365,7 @@ pub struct PaymentLinkDetails { pub merchant_logo: String, pub return_url: String, pub merchant_name: String, - pub order_details: Option>, + pub order_details: Option>, pub max_items_visible_after_collapse: i8, pub sdk_theme: Option, } @@ -3423,3 +3423,17 @@ pub struct PaymentLinkListResponse { // The list of payment link response objects pub data: Vec, } + +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct OrderDetailsWithStringAmount { + /// Name of the product that is being purchased + #[schema(max_length = 255, example = "shirt")] + pub product_name: String, + /// The quantity of the product to be purchased + #[schema(example = 1)] + pub quantity: u16, + /// the amount per quantity of product + pub amount: String, + /// Product Image link + pub product_img_link: Option, +} diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index cf49b1aad208..5963110c6324 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -241,6 +241,8 @@ pub enum StripeErrorCode { LockTimeout, #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "Merchant connector account is configured with invalid {config}")] InvalidConnectorConfiguration { config: String }, + #[error(error_type = StripeErrorType::HyperswitchError, code = "HE_01", message = "Failed to convert currency to minor unit")] + CurrencyConversionFailed, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -595,6 +597,7 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::InvalidConnectorConfiguration { config } => { Self::InvalidConnectorConfiguration { config } } + errors::ApiErrorResponse::CurrencyConversionFailed => Self::CurrencyConversionFailed, } } } @@ -662,7 +665,8 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::CurrencyNotSupported { .. } | Self::DuplicateCustomer | Self::PaymentMethodUnactivated - | Self::InvalidConnectorConfiguration { .. } => StatusCode::BAD_REQUEST, + | Self::InvalidConnectorConfiguration { .. } + | Self::CurrencyConversionFailed => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed | Self::PaymentLinkNotFound diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index ac51c5018df1..f94504cf274d 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -238,6 +238,8 @@ pub enum ApiErrorResponse { CurrencyNotSupported { message: String }, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_24", message = "Merchant connector account is configured with invalid {config}")] InvalidConnectorConfiguration { config: String }, + #[error(error_type = ErrorType::ValidationError, code = "HE_01", message = "Failed to convert currency to minor unit")] + CurrencyConversionFailed, } impl PTError for ApiErrorResponse { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 58eb0213cb64..fa9a5185790d 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -270,6 +270,9 @@ impl ErrorSwitch for ApiErrorRespon Self::InvalidConnectorConfiguration {config} => { AER::BadRequest(ApiError::new("IR", 24, format!("Merchant connector account is configured with invalid {config}"), None)) } + Self::CurrencyConversionFailed => { + AER::Unprocessable(ApiError::new("HE", 2, "Failed to convert currency to minor unit", None)) + } } } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 07fdf4ae4072..81b06f5f9aa8 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -85,8 +85,6 @@ pub async fn intiate_payment_link_flow( extract_payment_link_config(merchant_account.payment_link_config.clone())? }; - let order_details = validate_order_details(payment_intent.order_details)?; - let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { payment_create_return_url } else { @@ -102,12 +100,16 @@ pub async fn intiate_payment_link_flow( payment_intent.currency, payment_intent.client_secret, )?; + let order_details = validate_order_details(payment_intent.order_details, currency)?; let (default_sdk_theme, default_background_color) = (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); let payment_details = api_models::payments::PaymentLinkDetails { - amount: payment_intent.amount, + amount: currency + .to_currency_base_unit(payment_intent.amount) + .into_report() + .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?, currency, payment_id: payment_intent.payment_id, merchant_name: payment_link.custom_merchant_name.unwrap_or( @@ -236,8 +238,9 @@ pub fn check_payment_link_status(fulfillment_time: Option) -> fn validate_order_details( order_details: Option>>, + currency: api_models::enums::Currency, ) -> Result< - Option>, + Option>, error_stack::Report, > { let order_details = order_details @@ -256,14 +259,31 @@ fn validate_order_details( }) .transpose()?; - let updated_order_details = order_details.map(|mut order_details| { - for order in order_details.iter_mut() { - if order.product_img_link.is_none() { - order.product_img_link = Some(DEFAULT_PRODUCT_IMG.to_string()); + let updated_order_details = match order_details { + Some(mut order_details) => { + let mut order_details_amount_string_array: Vec< + api_models::payments::OrderDetailsWithStringAmount, + > = Vec::new(); + for order in order_details.iter_mut() { + let mut order_details_amount_string : api_models::payments::OrderDetailsWithStringAmount = Default::default(); + if order.product_img_link.is_none() { + order_details_amount_string.product_img_link = + Some(DEFAULT_PRODUCT_IMG.to_string()) + } else { + order_details_amount_string.product_img_link = order.product_img_link.clone() + }; + order_details_amount_string.amount = currency + .to_currency_base_unit(order.amount) + .into_report() + .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + order_details_amount_string.product_name = order.product_name.clone(); + order_details_amount_string.quantity = order.quantity; + order_details_amount_string_array.push(order_details_amount_string) } + Some(order_details_amount_string_array) } - order_details - }); + None => None, + }; Ok(updated_order_details) } From dc0e40d54bcff26c9996db6adc0a6071dc37ded0 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:07:07 +0000 Subject: [PATCH 224/443] chore(version): v1.102.1 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa1846f3b41..2d2c441578df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.102.1 (2023-12-18) + +### Bug Fixes + +- **connector:** [BOA/CYBERSOURCE] Update error handling ([#3156](https://github.com/juspay/hyperswitch/pull/3156)) ([`8e484dd`](https://github.com/juspay/hyperswitch/commit/8e484ddab8d3f4463299c7f7e8ce75b8dd628599)) +- **euclid_wasm:** Add function to retrieve keys for 3ds and surcharge decision manager ([#3160](https://github.com/juspay/hyperswitch/pull/3160)) ([`30fe9d1`](https://github.com/juspay/hyperswitch/commit/30fe9d19e4955035a370f8f9ce37963cdb76c68a)) +- **payment_link:** Added amount conversion to base unit based on currency ([#3162](https://github.com/juspay/hyperswitch/pull/3162)) ([`0fa61a9`](https://github.com/juspay/hyperswitch/commit/0fa61a9dd194c5b3688f8f68b056c263d92327d0)) +- Change prodintent name in dashboard metadata ([#3161](https://github.com/juspay/hyperswitch/pull/3161)) ([`8db3361`](https://github.com/juspay/hyperswitch/commit/8db3361d80f674a28a3916830a4b0c1c2b89776a)) + +### Refactors + +- **connector:** + - [Helcim] change error message from not supported to not implemented ([#2850](https://github.com/juspay/hyperswitch/pull/2850)) ([`41b5a82`](https://github.com/juspay/hyperswitch/commit/41b5a82bafa9b0392bb43ed268fefc5187b48636)) + - [Forte] change error message from not supported to not implemented ([#2847](https://github.com/juspay/hyperswitch/pull/2847)) ([`3fc0e2d`](https://github.com/juspay/hyperswitch/commit/3fc0e2d8195948d50f735df5192ae0f8431b432b)) + - [Cryptopay] change error message from not supported to not implemented ([#2846](https://github.com/juspay/hyperswitch/pull/2846)) ([`2d895be`](https://github.com/juspay/hyperswitch/commit/2d895be9856d17cd923665568aa9b6e54fc1a305)) +- **router:** [ACI] change payment error message from not supported to not implemented error ([#2837](https://github.com/juspay/hyperswitch/pull/2837)) ([`cc12e8a`](https://github.com/juspay/hyperswitch/commit/cc12e8a2435e5e47eeec77c620c747b156a3e16b)) +- **users:** Rename `user_roles` and `dashboard_metadata` columns ([#3135](https://github.com/juspay/hyperswitch/pull/3135)) ([`e3589e6`](https://github.com/juspay/hyperswitch/commit/e3589e641c8a0b3b690b82f09a61d512db2d9932)) + +**Full Changelog:** [`v1.102.0+hotfix.1...v1.102.1`](https://github.com/juspay/hyperswitch/compare/v1.102.0+hotfix.1...v1.102.1) + +- - - + + ## 1.102.0 (2023-12-17) ### Features From 3662f1f6190a439187fad7e341c25359d8c30dd2 Mon Sep 17 00:00:00 2001 From: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:35:28 +0530 Subject: [PATCH 225/443] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index db8e820ef142..dfa77ebe0666 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The single API to access payment ecosystems across 130+ countries

Quick Start Guide • + Local Setup GuideFast Integration for Stripe UsersSupported FeaturesFAQs From ce5514eadfce240bc4cefb472405f37432a8507b Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Tue, 19 Dec 2023 13:38:14 +0530 Subject: [PATCH 226/443] docs(connector): update connector integration documentation (#3041) Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- add_connector.md | 602 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 521 insertions(+), 81 deletions(-) diff --git a/add_connector.md b/add_connector.md index da09ae0024e7..7fc3dcb27d14 100644 --- a/add_connector.md +++ b/add_connector.md @@ -9,6 +9,14 @@ This is a guide to contributing new connector to Router. This guide includes ins - Understanding of the Connector APIs which you wish to integrate with Router - Setup of Router repository and running it on local - Access to API credentials for testing the Connector API (you can quickly sign up for sandbox/uat credentials by visiting the website of the connector you wish to integrate) +- Ensure that you have the nightly toolchain installed because the connector template script includes code formatting. + + Install it using `rustup`: + + ```bash + rustup toolchain install nightly + ``` + In Router, there are Connectors and Payment Methods, examples of both are shown below from which the difference is apparent. @@ -17,22 +25,17 @@ In Router, there are Connectors and Payment Methods, examples of both are shown A connector is an integration to fulfill payments. Related use cases could be any of the below - Payment processor (Stripe, Adyen, ChasePaymentTech etc.,) -- Fraud and Risk management platform (like Ravelin, Riskified etc.,) +- Fraud and Risk management platform (like Signifyd, Riskified etc.,) - Payment network (Visa, Master) - Payment authentication services (Cardinal etc.,) - Router supports "Payment Processors" right now. Support will be extended to the other categories in the near future. +Currently, the router is compatible with 'Payment Processors' and 'Fraud and Risk Management' platforms. Support for additional categories will be expanded in the near future. ### What is a Payment Method ? -Each Connector (say, a Payment Processor) could support multiple payment methods +Every Payment Processor has the capability to accommodate various payment methods. Refer to the [Hyperswitch Payment matrix](https://hyperswitch.io/pm-list) to discover the supported processors and payment methods. -- **Cards :** Bancontact , Knet, Mada -- **Bank Transfers :** EPS , giropay, sofort -- **Bank Direct Debit :** Sepa direct debit -- **Wallets :** Apple Pay , Google Pay , Paypal - -Cards and Bank Transfer payment methods are already included in Router. Hence, adding a new connector which offers payment_methods available in Router is easy and requires almost no breaking changes. -Adding a new payment method (say Wallets or Bank Direct Debit) might require some changes in core business logic of Router, which we are actively working upon. +The above mentioned payment methods are already included in Router. Hence, adding a new connector which offers payment_methods available in Router is easy and requires almost no breaking changes. +Adding a new payment method might require some changes in core business logic of Router, which we are actively working upon. ## How to Integrate a Connector @@ -46,8 +49,7 @@ Below is a step-by-step tutorial for integrating a new connector. ### **Generate the template** ```bash -cd scripts -bash add_connector.sh +sh scripts/add_connector.sh ``` For this tutorial `` would be `checkout`. @@ -81,50 +83,59 @@ For example, in case of checkout, the [request](https://api-reference.checkout.c Now let's implement Request type for checkout -```rust -#[derive(Debug, Serialize)] -pub struct CheckoutPaymentsRequest { - pub source: Source, - pub amount: i64, - pub currency: String, - #[serde(default = "generate_processing_channel_id")] - pub processing_channel_id: Cow<'static, str>, -} - -fn generate_processing_channel_id() -> Cow<'static, str> { - "pc_e4mrdrifohhutfurvuawughfwu".into() -} -``` - -Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored. - -Here processing_channel_id, is specific to checkout and implementations of such functions should be inside the checkout directory. -Let's define `Source` - ```rust #[derive(Debug, Serialize)] pub struct CardSource { #[serde(rename = "type")] - pub source_type: Option, - pub number: Option, - pub expiry_month: Option, - pub expiry_year: Option, + pub source_type: CheckoutSourceTypes, + pub number: cards::CardNumber, + pub expiry_month: Secret, + pub expiry_year: Secret, + pub cvv: Secret, } #[derive(Debug, Serialize)] #[serde(untagged)] -pub enum Source { +pub enum PaymentSource { Card(CardSource), - // TODO: Add other sources here. + Wallets(WalletSource), + ApplePayPredecrypt(Box), +} + +#[derive(Debug, Serialize)] +pub struct PaymentsRequest { + pub source: PaymentSource, + pub amount: i64, + pub currency: String, + pub processing_channel_id: Secret, + #[serde(rename = "3ds")] + pub three_ds: CheckoutThreeDS, + #[serde(flatten)] + pub return_url: ReturnUrl, + pub capture: bool, + pub reference: String, } ``` -`Source` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From` needs to be implemented. +Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored. + +Here processing_channel_id, is specific to checkout and implementations of such functions should be inside the checkout directory. +Let's define `PaymentSource` + +`PaymentSource` is an enum type. Request types will need to derive `Serialize` and response types will need to derive `Deserialize`. For request types `From` needs to be implemented. +For request types that involve an amount, the implementation of `TryFrom<&ConnectorRouterData<&T>>` is required: + +```rust +impl TryFrom<&CheckoutRouterData<&T>> for PaymentsRequest +``` +else ```rust -impl<'a> From<&types::RouterData<'a>> for CheckoutRequestType +impl TryFrom for PaymentsRequest ``` +where `T` is a generic type which can be `types::PaymentsAuthorizeRouterData`, `types::PaymentsCaptureRouterData`, etc. + In this impl block we build the request type from RouterData which will almost always contain all the required information you need for payment processing. `RouterData` contains all the information required for processing the payment. @@ -165,39 +176,56 @@ While implementing the Response Type, the important Enum to be defined for every It stores the different status types that the connector can give in its response that is listed in its API spec. Below is the definition for checkout ```rust -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Default, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] pub enum CheckoutPaymentStatus { Authorized, + #[default] Pending, #[serde(rename = "Card Verified")] CardVerified, Declined, + Captured, } ``` The important part is mapping it to the Router status codes. ```rust -impl From for enums::AttemptStatus { - fn from(item: CheckoutPaymentStatus) -> Self { - match item { - CheckoutPaymentStatus::Authorized => enums::AttemptStatus::Charged, - CheckoutPaymentStatus::Declined => enums::AttemptStatus::Failure, - CheckoutPaymentStatus::Pending => enums::AttemptStatus::Authorizing, - CheckoutPaymentStatus::CardVerified => enums::AttemptStatus::Pending, +impl ForeignFrom<(CheckoutPaymentStatus, Option)> for enums::AttemptStatus { + fn foreign_from(item: (CheckoutPaymentStatus, Option)) -> Self { + let (status, balances) = item; + + match status { + CheckoutPaymentStatus::Authorized => { + if let Some(Balances { + available_to_capture: 0, + }) = balances + { + Self::Charged + } else { + Self::Authorized + } + } + CheckoutPaymentStatus::Captured => Self::Charged, + CheckoutPaymentStatus::Declined => Self::Failure, + CheckoutPaymentStatus::Pending => Self::AuthenticationPending, + CheckoutPaymentStatus::CardVerified => Self::Pending, } } } ``` +If you're converting ConnectorPaymentStatus to AttemptStatus without any additional conditions, you can employ the `impl From for enums::AttemptStatus`. -Note: `enum::AttemptStatus` is Router status. +Note: A payment intent can have multiple payment attempts. `enums::AttemptStatus` represents the status of a payment attempt. -Router status are given below +Some of the attempt status are given below -- **Charged :** The amount has been debited -- **PendingVBV :** Pending but verified by visa -- **Failure :** The payment Failed -- **Authorizing :** In the process of authorizing. +- **Charged :** The payment attempt has succeeded. +- **Pending :** Payment is in processing state. +- **Failure :** The payment attempt has failed. +- **Authorized :** Payment is authorized. Authorized payment can be voided, captured and partial captured. +- **AuthenticationPending :** Customer action is required. +- **Voided :** The payment was voided and never captured; the funds were returned to the customer. It is highly recommended that the default status is Pending. Only explicit failure and explicit success from the connector shall be marked as success or failure respectively. @@ -213,26 +241,119 @@ impl Default for CheckoutPaymentStatus { Below is rest of the response type implementation for checkout ```rust - -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct CheckoutPaymentsResponse { +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct PaymentsResponse { id: String, - amount: i64, + amount: Option, + action_id: Option, status: CheckoutPaymentStatus, + #[serde(rename = "_links")] + links: Links, + balances: Option, + reference: Option, + response_code: Option, + response_summary: Option, } -impl<'a> From> for types::RouterData<'a> { - fn from(item: types::ResponseRouterData<'a, CheckoutPaymentsResponse>) -> Self { - types::RouterData { - connector_transaction_id: Some(item.response.id), - amount_received: Some(item.response.amount), - status: enums::Status::from(item.response.status), +#[derive(Deserialize, Debug)] +pub struct ActionResponse { + #[serde(rename = "id")] + pub action_id: String, + pub amount: i64, + #[serde(rename = "type")] + pub action_type: ActionType, + pub approved: Option, + pub reference: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum PaymentsResponseEnum { + ActionResponse(Vec), + PaymentResponse(Box), +} + +impl TryFrom> + for types::PaymentsAuthorizeRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsResponseRouterData, + ) -> Result { + let redirection_data = item.response.links.redirect.map(|href| { + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); + let status = enums::AttemptStatus::foreign_from(( + item.response.status, + item.data.request.capture_method, + )); + let error_response = if status == enums::AttemptStatus::Failure { + Some(types::ErrorResponse { + status_code: item.http_code, + code: item + .response + .response_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .response_summary + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.response_summary, + attempt_status: None, + connector_transaction_id: None, + }) + } else { + None + }; + let payments_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), + redirection_data, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + item.response.reference.unwrap_or(item.response.id), + ), + }; + Ok(Self { + status, + response: error_response.map_or_else(|| Ok(payments_response_data), Err), ..item.data - } + }) } } ``` +Using an enum for a response struct in Rust is not recommended due to potential deserialization issues where the deserializer attempts to deserialize into all the enum variants. A preferable alternative is to employ a separate enum for the possible response variants and include it as a field within the response struct. + +Some recommended fields that needs to be set on connector request and response + +- **connector_request_reference_id :** Most of the connectors anticipate merchants to include their own reference ID in payment requests. For instance, the merchant's reference ID in the checkout `PaymentRequest` is specified as `reference`. + +```rust + reference: item.router_data.connector_request_reference_id.clone(), +``` +- **connector_response_reference_id :** Merchants might face ambiguity when deciding which ID to use in the connector dashboard for payment identification. It is essential to populate the connector_response_reference_id with the appropriate reference ID, allowing merchants to recognize the transaction. This field can be linked to either `merchant_reference` or `connector_transaction_id`, depending on the field that the connector dashboard search functionality supports. + +```rust + connector_response_reference_id: item.response.reference.or(Some(item.response.id)) +``` + +- **resource_id :** The connector assigns an identifier to a payment attempt, referred to as `connector_transaction_id`. This identifier is represented as an enum variant for the `resource_id`. If the connector does not provide a `connector_transaction_id`, the resource_id is set to `NoResponseId`. + +```rust + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()), +``` +- **redirection_data :** For the implementation of a redirection flow (3D Secure, bank redirects, etc.), assign the redirection link to the `redirection_data`. + +```rust + let redirection_data = item.response.links.redirect.map(|href| { + services::RedirectForm::from((href.redirection_url, services::Method::Get)) + }); +``` + + And finally the error type implementation ```rust @@ -251,23 +372,250 @@ Similarly for every API endpoint you can implement request and response types. The `mod.rs` file contains the trait implementations where we use the types in transformers. -There are four types of tasks that are done by implementing traits: - -- **Payment :** For making/initiating payments -- **PaymentSync :** For checking status of the payment -- **Refund :** For initiating refund -- **RefundSync :** For checking status of the Refund. - We create a struct with the connector name and have trait implementations for it. The following trait implementations are mandatory -- **ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error message, id. -- **Payment :** Trait Relationship, has impl block. -- **PaymentAuthorize :** Trait Relationship, has impl block. -- **ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. -- **Refund :** Trait Relationship, has empty body. -- **RefundExecute :** Trait Relationship, has empty body. -- **RefundSync :** Trait Relationship, has empty body. +**ConnectorCommon :** contains common description of the connector, like the base endpoint, content-type, error response handling, id, currency unit. + +Within the `ConnectorCommon` trait, you'll find the following methods : + + - `id` method corresponds directly to the connector name. + ```rust + fn id(&self) -> &'static str { + "checkout" + } + ``` + - `get_currency_unit` method anticipates you to [specify the accepted currency unit](#set-the-currency-unit) for the connector. + ```rust + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + ``` + - `common_get_content_type` method requires you to provide the accepted content type for the connector API. + ```rust + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + ``` + - `get_auth_header` method accepts common HTTP Authorization headers that are accepted in all `ConnectorIntegration` flows. + ```rust + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth: checkout::CheckoutAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", auth.api_secret.peek()).into_masked(), + )]) + } + ``` + + - `base_url` method is for fetching the base URL of connector's API. Base url needs to be consumed from configs. + ```rust + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.checkout.base_url.as_ref() + } + ``` + - `build_error_response` method is common error response handling for a connector if it is same in all cases + + ```rust + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: checkout::ErrorResponse = if res.response.is_empty() { + let (error_codes, error_type) = if res.status_code == 401 { + ( + Some(vec!["Invalid api key".to_string()]), + Some("invalid_api_key".to_string()), + ) + } else { + (None, None) + }; + checkout::ErrorResponse { + request_id: None, + error_codes, + error_type, + } + } else { + res.response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)? + }; + + router_env::logger::info!(error_response=?response); + let errors_list = response.error_codes.clone().unwrap_or_default(); + let option_error_code_message = conn_utils::get_error_code_error_message_based_on_priority( + self.clone(), + errors_list + .into_iter() + .map(|errors| errors.into()) + .collect(), + ); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: option_error_code_message + .clone() + .map(|error_code_message| error_code_message.error_code) + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: option_error_code_message + .map(|error_code_message| error_code_message.error_message) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: response + .error_codes + .map(|errors| errors.join(" & ")) + .or(response.error_type), + attempt_status: None, + connector_transaction_id: None, + }) + } + ``` + +**ConnectorIntegration :** For every api endpoint contains the url, using request transform and response transform and headers. +Within the `ConnectorIntegration` trait, you'll find the following methods implemented(below mentioned is example for authorized flow): + +- `get_url` method defines endpoint for authorize flow, base url is consumed from `ConnectorCommon` trait. + +```rust + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}{}", self.base_url(connectors), "payments")) + } +``` +- `get_headers` method accepts HTTP headers that are accepted for authorize flow. In this context, it is utilized from the `ConnectorCommonExt` trait, as the connector adheres to common headers across various flows. + +```rust + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } +``` + +- `get_request_body` method calls transformers where hyperswitch payment request data is transformed into connector payment request. For constructing the request body have a function `log_and_get_request_body` that allows generic argument which is the struct that is passed as the body for connector integration, and a function that can be use to encode it into String. We log the request in this function, as the struct will be intact and the masked values will be masked. + +```rust + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = checkout::CheckoutRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = checkout::PaymentsRequest::try_from(&connector_router_data)?; + let checkout_req = types::RequestBody::log_and_get_request_body( + &connector_req, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(checkout_req)) + } +``` + +- `build_request` method assembles the API request by providing the method, URL, headers, and request body as parameters. +```rust + fn build_request( + &self, + req: &types::RouterData< + api::Authorize, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } +``` +- `handle_response` method calls transformers where connector response data is transformed into hyperswitch response. +```rust + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { + let response: checkout::PaymentsResponse = res + .response + .parse_struct("PaymentIntentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } +``` +- `get_error_response` method to manage error responses. As the handling of checkout errors remains consistent across various flows, we've incorporated it from the `build_error_response` method within the `ConnectorCommon` trait. +```rust + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +``` +**ConnectorCommonExt :** An enhanced trait for `ConnectorCommon` that enables functions with a generic type. This trait includes the `build_headers` method, responsible for constructing both the common headers and the Authorization headers (retrieved from the `get_auth_header` method), returning them as a vector. + +```rust + where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} +``` + +**Payment :** This trait includes several other traits and is meant to represent the functionality related to payments. + +**PaymentAuthorize :** This trait extends the `api::ConnectorIntegration `trait with specific types related to payment authorization. + +**PaymentCapture :** This trait extends the `api::ConnectorIntegration `trait with specific types related to manual payment capture. + +**PaymentSync :** This trait extends the `api::ConnectorIntegration `trait with specific types related to payment retrieve. + +**Refund :** This trait includes several other traits and is meant to represent the functionality related to Refunds. + +**RefundExecute :** This trait extends the `api::ConnectorIntegration `trait with specific types related to refunds create. + +**RefundSync :** This trait extends the `api::ConnectorIntegration `trait with specific types related to refunds retrieve. + And the below derive traits @@ -277,13 +625,105 @@ And the below derive traits There is a trait bound to implement refunds, if you don't want to implement refunds you can mark them as `todo!()` but code panics when you initiate refunds then. -Don’t forget to add logs lines in appropriate places. Refer to other connector code for trait implementations. Mostly the 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. +### **Set the currency Unit** +The `get_currency_unit` function, part of the ConnectorCommon trait, enables connectors to specify their accepted currency unit as either `Base` or `Minor`. For instance, Paypal designates its currency in the base unit (for example, USD), whereas Hyperswitch processes amounts in the minor unit (for example, cents). If a connector accepts amounts in the base unit, conversion is required, as illustrated. + +``` rust +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for PaypalRouterData +{ + type Error = error_stack::Report; + fn try_from( + (currency_unit, currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; + Ok(Self { + amount, + router_data: item, + }) + } +} +``` + +**Note:** Since the amount is being converted in the aforementioned `try_from`, it is necessary to retrieve amounts from `ConnectorRouterData` in all other `try_from` instances. + +### **Connector utility functions** + +In the `connector/utils.rs` file, you'll discover utility functions that aid in constructing connector requests and responses. We highly recommend using these helper functions for retrieving payment request fields, such as `get_billing_country`, `get_browser_info`, and `get_expiry_date_as_yyyymm`, as well as for validations, including `is_three_ds`, `is_auto_capture`, and more. + +```rust + let json_wallet_data: CheckoutGooglePayData =wallet_data.get_wallet_token_as_json()?; +``` + ### **Test the connector** -Try running the tests in `crates/router/tests/connectors/{{connector-name}}.rs`. +The template code script generates a test file for the connector, containing 20 sanity tests. We anticipate that you will implement these tests when adding a new connector. + +```rust +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[serial_test::serial] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} +``` + +Utility functions for tests are also available at `tests/connector/utils`. These functions enable you to write tests with ease. + +```rust + /// For initiating payments when `CaptureMethod` is set to `Manual` + /// This doesn't complete the transaction, `PaymentsCapture` needs to be done manually + async fn authorize_payment( + &self, + payment_data: Option, + payment_info: Option, + ) -> Result> { + let integration = self.get_data().connector.get_connector_integration(); + let mut request = self.generate_data( + types::PaymentsAuthorizeData { + confirm: true, + capture_method: Some(diesel_models::enums::CaptureMethod::Manual), + ..(payment_data.unwrap_or(PaymentAuthorizeType::default().0)) + }, + payment_info, + ); + let tx: oneshot::Sender<()> = oneshot::channel().0; + let state = routes::AppState::with_storage( + Settings::new().unwrap(), + StorageImpl::PostgresqlTest, + tx, + Box::new(services::MockApiClient), + ) + .await; + integration.execute_pretasks(&mut request, &state).await?; + Box::pin(call_connector(request, integration)).await + } +``` + +Prior to executing tests in the shell, ensure that the API keys are configured in `crates/router/tests/connectors/sample_auth.toml` and set the environment variable `CONNECTOR_AUTH_FILE_PATH` using the export command. Avoid pushing code with exposed API keys. + +```rust + export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml" + cargo test --package router --test connectors -- checkout --test-threads=1 +``` All tests should pass and add appropriate tests for connector specific payment flows. ### **Build payment request and response from json schema** From 583d7b87a711102e4e62417f3191ac837886eca9 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:14:47 +0530 Subject: [PATCH 227/443] feat(postman): [Prophetpay] Add test cases (#2946) --- .github/secrets/connector_auth.toml.gpg | Bin 3395 -> 3435 bytes postman/collection-dir/prophetpay/.auth.json | 22 ++++ .../prophetpay/.event.meta.json | 3 + postman/collection-dir/prophetpay/.info.json | 9 ++ postman/collection-dir/prophetpay/.meta.json | 6 ++ .../collection-dir/prophetpay/.variable.json | 96 ++++++++++++++++++ .../prophetpay/Flow Testcases/.meta.json | 3 + .../Flow Testcases/Happy Cases/.meta.json | 6 ++ .../.meta.json | 3 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 71 +++++++++++++ .../Payments - Create/request.json | 47 +++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 71 +++++++++++++ .../Payments - Retrieve/request.json | 28 +++++ .../Payments - Retrieve/response.json | 1 + .../.meta.json | 7 ++ .../Payments - Confirm/.event.meta.json | 6 ++ .../Payments - Confirm/event.prerequest.js | 0 .../Payments - Confirm/event.test.js | 74 ++++++++++++++ .../Payments - Confirm/request.json | 70 +++++++++++++ .../Payments - Confirm/response.json | 1 + .../Payments - Create/.event.meta.json | 6 ++ .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 71 +++++++++++++ .../Payments - Create/request.json | 40 ++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 5 + .../Payments - Retrieve/event.test.js | 71 +++++++++++++ .../Payments - Retrieve/request.json | 33 ++++++ .../Payments - Retrieve/response.json | 1 + .../Flow Testcases/QuickStart/.meta.json | 9 ++ .../API Key - Create/.event.meta.json | 3 + .../QuickStart/API Key - Create/event.test.js | 46 +++++++++ .../QuickStart/API Key - Create/request.json | 47 +++++++++ .../QuickStart/API Key - Create/response.json | 1 + .../.event.meta.json | 3 + .../Merchant Account - Create/event.test.js | 56 ++++++++++ .../Merchant Account - Create/request.json | 91 +++++++++++++++++ .../Merchant Account - Create/response.json | 1 + .../.event.meta.json | 3 + .../Payment Connector - Create/event.test.js | 39 +++++++ .../Payment Connector - Create/request.json | 90 ++++++++++++++++ .../Payment Connector - Create/response.json | 1 + .../Payments - Create/.event.meta.json | 3 + .../Payments - Create/event.test.js | 61 +++++++++++ .../QuickStart/Payments - Create/request.json | 47 +++++++++ .../Payments - Create/response.json | 1 + .../Payments - Retrieve/.event.meta.json | 3 + .../Payments - Retrieve/event.test.js | 61 +++++++++++ .../Payments - Retrieve/request.json | 22 ++++ .../Payments - Retrieve/response.json | 1 + .../prophetpay/Health check/.meta.json | 3 + .../Health check/New Request/.event.meta.json | 3 + .../Health check/New Request/event.test.js | 4 + .../Health check/New Request/request.json | 8 ++ .../Health check/New Request/response.json | 1 + .../prophetpay/event.prerequest.js | 0 .../collection-dir/prophetpay/event.test.js | 13 +++ 60 files changed, 1380 insertions(+) create mode 100644 postman/collection-dir/prophetpay/.auth.json create mode 100644 postman/collection-dir/prophetpay/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/.info.json create mode 100644 postman/collection-dir/prophetpay/.meta.json create mode 100644 postman/collection-dir/prophetpay/.variable.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.prerequest.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Confirm/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.prerequest.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Create/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/API Key - Create/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Merchant Account - Create/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payment Connector - Create/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Create/response.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/event.test.js create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/request.json create mode 100644 postman/collection-dir/prophetpay/Flow Testcases/QuickStart/Payments - Retrieve/response.json create mode 100644 postman/collection-dir/prophetpay/Health check/.meta.json create mode 100644 postman/collection-dir/prophetpay/Health check/New Request/.event.meta.json create mode 100644 postman/collection-dir/prophetpay/Health check/New Request/event.test.js create mode 100644 postman/collection-dir/prophetpay/Health check/New Request/request.json create mode 100644 postman/collection-dir/prophetpay/Health check/New Request/response.json create mode 100644 postman/collection-dir/prophetpay/event.prerequest.js create mode 100644 postman/collection-dir/prophetpay/event.test.js diff --git a/.github/secrets/connector_auth.toml.gpg b/.github/secrets/connector_auth.toml.gpg index 7da9189ade58a1bab00649a724f5f1c4f26fee7f..ce62370c1494e7173b9e45d2276ad58269e51e72 100644 GIT binary patch literal 3435 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 Date: Tue, 19 Dec 2023 17:11:41 +0530 Subject: [PATCH 228/443] fix(connector): [NMI] Fix response deserialization for vault id creation (#3166) --- crates/router/src/connector/nmi/transformers.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index b0403d11e3e4..5dfcdcf8b99d 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -129,7 +129,7 @@ fn get_card_details( pub struct NmiVaultResponse { pub response: Response, pub responsetext: String, - pub customer_vault_id: String, + pub customer_vault_id: Option, pub response_code: String, } @@ -178,7 +178,11 @@ impl )? .to_string(), currency: currency_data, - customer_vault_id: item.response.customer_vault_id, + customer_vault_id: item.response.customer_vault_id.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "customer_vault_id", + }, + )?, public_key: auth_type.public_key.ok_or( errors::ConnectorError::InvalidConnectorConfig { config: "public_key", From dc589d580f1382874bc755d3719bd3244fdedc67 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:27:25 +0530 Subject: [PATCH 229/443] fix(users): send correct `user_role` values in `switch_merchant` response (#3167) --- crates/router/src/core/user.rs | 111 ++++++++++++++++---------------- crates/router/src/utils/user.rs | 27 ++------ 2 files changed, 63 insertions(+), 75 deletions(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 6401e1ea6ca7..a13eba6ed5b5 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -506,69 +506,72 @@ pub async fn switch_merchant_id( request: user_api::SwitchMerchantIdRequest, user_from_token: auth::UserFromToken, ) -> UserResponse { - if !utils::user_role::is_internal_role(&user_from_token.role_id) { - let merchant_list = - utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) - .await?; - if !merchant_list.contains(&request.merchant_id) { - return Err(UserErrors::InvalidRoleOperation.into()) - .attach_printable("User doesn't have access to switch"); - } - } - if user_from_token.merchant_id == request.merchant_id { return Err(UserErrors::InvalidRoleOperation.into()) .attach_printable("User switch to same merchant id."); } - let user = state + let user_roles = state .store - .find_user_by_id(&user_from_token.user_id) + .list_user_roles_by_user_id(&user_from_token.user_id) .await .change_context(UserErrors::InternalServerError)?; - let key_store = state - .store - .get_merchant_key_store_by_merchant_id( - request.merchant_id.as_str(), - &state.store.get_master_key().to_vec().into(), - ) - .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(UserErrors::MerchantIdNotFound) - } else { - e.change_context(UserErrors::InternalServerError) - } - })?; + let active_user_roles = user_roles + .into_iter() + .filter(|role| role.status == UserStatus::Active) + .collect::>(); - let _org_id = state - .store - .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) - .await - .map_err(|e| { - if e.current_context().is_db_not_found() { - e.change_context(UserErrors::MerchantIdNotFound) - } else { - e.change_context(UserErrors::InternalServerError) - } - })? - .organization_id; + let user = user_from_token.get_user(state.clone()).await?.into(); - let user = domain::UserFromStorage::from(user); - let user_role = state - .store - .find_user_role_by_user_id(user.get_user_id()) - .await - .change_context(UserErrors::InternalServerError)?; + let (token, role_id) = if utils::user_role::is_internal_role(&user_from_token.role_id) { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + request.merchant_id.as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; - let token = utils::user::generate_jwt_auth_token_with_custom_merchant_id( - state, - &user, - &user_role, - request.merchant_id.clone(), - ) - .await?; + let org_id = state + .store + .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .organization_id; + + let token = utils::user::generate_jwt_auth_token_with_custom_role_attributes( + state, + &user, + request.merchant_id.clone(), + org_id, + user_from_token.role_id.clone(), + ) + .await?; + (token, user_from_token.role_id) + } else { + let user_role = active_user_roles + .iter() + .find(|role| role.merchant_id == request.merchant_id) + .ok_or(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User doesn't have access to switch")?; + + let token = utils::user::generate_jwt_auth_token(state, &user, user_role).await?; + (token, user_role.role_id.clone()) + }; Ok(ApplicationResponse::Json( user_api::SwitchMerchantResponse { @@ -577,8 +580,8 @@ pub async fn switch_merchant_id( email: user.get_email(), user_id: user.get_user_id().to_string(), verification_days_left: None, - user_role: user_role.role_id, - merchant_id: user_role.merchant_id, + user_role: role_id, + merchant_id: request.merchant_id, }, )) } @@ -620,7 +623,7 @@ pub async fn list_merchant_ids_for_user( user: auth::UserFromToken, ) -> UserResponse> { Ok(ApplicationResponse::Json( - utils::user::get_merchant_ids_for_user(state, &user.user_id).await?, + utils::user_role::get_merchant_ids_for_user(state, &user.user_id).await?, )) } diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 5f765028014f..a115fa2a2d8a 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,5 +1,5 @@ use api_models::user as user_api; -use diesel_models::{enums::UserStatus, user_role::UserRole}; +use diesel_models::user_role::UserRole; use error_stack::ResultExt; use masking::Secret; @@ -55,22 +55,6 @@ impl UserFromToken { } } -pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult> { - Ok(state - .store - .list_user_roles_by_user_id(user_id) - .await - .change_context(UserErrors::InternalServerError)? - .into_iter() - .filter_map(|ele| { - if ele.status == UserStatus::Active { - return Some(ele.merchant_id); - } - None - }) - .collect()) -} - pub async fn generate_jwt_auth_token( state: AppState, user: &UserFromStorage, @@ -87,18 +71,19 @@ pub async fn generate_jwt_auth_token( Ok(Secret::new(token)) } -pub async fn generate_jwt_auth_token_with_custom_merchant_id( +pub async fn generate_jwt_auth_token_with_custom_role_attributes( state: AppState, user: &UserFromStorage, - user_role: &UserRole, merchant_id: String, + org_id: String, + role_id: String, ) -> UserResult> { let token = AuthToken::new_token( user.get_user_id().to_string(), merchant_id, - user_role.role_id.clone(), + role_id, &state.conf, - user_role.org_id.to_owned(), + org_id, ) .await?; Ok(Secret::new(token)) From 45ba128b6ab39f513dd114567d9915acf0eaea20 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 19 Dec 2023 19:00:22 +0530 Subject: [PATCH 230/443] fix(connector): Connector wise validation for zero auth flow (#3159) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/aci.rs | 11 +++++++ crates/router/src/connector/airwallex.rs | 10 ++++++ .../router/src/connector/authorizedotnet.rs | 14 +++++++++ crates/router/src/connector/bambora.rs | 14 +++++++++ crates/router/src/connector/bankofamerica.rs | 14 +++++++++ crates/router/src/connector/bitpay.rs | 14 +++++++++ crates/router/src/connector/bluesnap.rs | 14 +++++++++ crates/router/src/connector/boku.rs | 14 +++++++++ crates/router/src/connector/braintree.rs | 14 +++++++++ crates/router/src/connector/cashtocode.rs | 14 +++++++++ crates/router/src/connector/checkout.rs | 14 +++++++++ crates/router/src/connector/coinbase.rs | 14 +++++++++ crates/router/src/connector/cryptopay.rs | 14 +++++++++ crates/router/src/connector/dlocal.rs | 14 +++++++++ crates/router/src/connector/dummyconnector.rs | 14 +++++++++ crates/router/src/connector/fiserv.rs | 14 +++++++++ crates/router/src/connector/forte.rs | 14 +++++++++ crates/router/src/connector/globalpay.rs | 14 +++++++++ crates/router/src/connector/globepay.rs | 14 +++++++++ crates/router/src/connector/helcim.rs | 31 +++++++++++-------- crates/router/src/connector/iatapay.rs | 14 +++++++++ crates/router/src/connector/klarna.rs | 14 +++++++++ crates/router/src/connector/mollie.rs | 14 +++++++++ crates/router/src/connector/multisafepay.rs | 14 +++++++++ crates/router/src/connector/nexinets.rs | 14 +++++++++ crates/router/src/connector/nmi.rs | 26 +++++++++------- crates/router/src/connector/noon.rs | 14 +++++++++ crates/router/src/connector/nuvei.rs | 14 +++++++++ crates/router/src/connector/opayo.rs | 14 +++++++++ crates/router/src/connector/opennode.rs | 14 +++++++++ crates/router/src/connector/payeezy.rs | 14 +++++++++ crates/router/src/connector/payme.rs | 14 +++++++++ crates/router/src/connector/paypal.rs | 14 +++++++++ crates/router/src/connector/payu.rs | 14 +++++++++ crates/router/src/connector/placetopay.rs | 14 +++++++++ crates/router/src/connector/powertranz.rs | 14 +++++++++ crates/router/src/connector/prophetpay.rs | 14 +++++++++ crates/router/src/connector/rapyd.rs | 14 +++++++++ crates/router/src/connector/riskified.rs | 17 ++++++++-- crates/router/src/connector/shift4.rs | 14 +++++++++ crates/router/src/connector/signifyd.rs | 17 ++++++++-- crates/router/src/connector/square.rs | 14 +++++++++ crates/router/src/connector/stax.rs | 14 +++++++++ crates/router/src/connector/trustpay.rs | 14 +++++++++ crates/router/src/connector/tsys.rs | 14 +++++++++ crates/router/src/connector/volt.rs | 14 +++++++++ crates/router/src/connector/wise.rs | 14 +++++++++ crates/router/src/connector/worldline.rs | 14 +++++++++ crates/router/src/connector/worldpay.rs | 14 +++++++++ crates/router/src/connector/zen.rs | 11 +++++++ 50 files changed, 696 insertions(+), 29 deletions(-) diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 69a2c5364359..5c65a8a2726d 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -137,6 +137,17 @@ impl > for Aci { // Issue: #173 + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Aci".to_string()).into()) + } } impl diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index 15b216aaddb7..4fc813d628ec 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -126,6 +126,16 @@ impl types::PaymentsResponseData, > for Airwallex { + fn build_request( + &self, + _req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Airwallex".to_string()) + .into(), + ) + } } impl api::PaymentToken for Airwallex {} diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 88dc000d3ba2..9c8d2f470246 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -118,6 +118,20 @@ impl > for Authorizedotnet { // Issue: #173 + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for Authorizedotnet".to_string(), + ) + .into()) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/bambora.rs b/crates/router/src/connector/bambora.rs index a8e726a3e4f9..38d89bbc0e57 100644 --- a/crates/router/src/connector/bambora.rs +++ b/crates/router/src/connector/bambora.rs @@ -139,6 +139,20 @@ impl types::PaymentsResponseData, > for Bambora { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Bambora".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Bambora {} diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index b13bb299c2df..8bee764b1f72 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -283,6 +283,20 @@ impl types::PaymentsResponseData, > for Bankofamerica { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for Bankofamerica".to_string(), + ) + .into()) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/bitpay.rs b/crates/router/src/connector/bitpay.rs index 5ccd95c62316..4a8108d04b83 100644 --- a/crates/router/src/connector/bitpay.rs +++ b/crates/router/src/connector/bitpay.rs @@ -146,6 +146,20 @@ impl types::PaymentsResponseData, > for Bitpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Bitpay".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 9e1dcac28cf8..edcad00c9830 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -233,6 +233,20 @@ impl types::PaymentsResponseData, > for Bluesnap { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Bluesnap".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Bluesnap {} diff --git a/crates/router/src/connector/boku.rs b/crates/router/src/connector/boku.rs index 0da32fde58eb..39566e08d470 100644 --- a/crates/router/src/connector/boku.rs +++ b/crates/router/src/connector/boku.rs @@ -170,6 +170,20 @@ impl types::PaymentsResponseData, > for Boku { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Boku".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 4c6f862e3928..4f2686abb136 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -390,6 +390,20 @@ impl > for Braintree { // Not Implemented (R) + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Braintree".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index 0b5634e0fba6..5a628f775b6b 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -158,6 +158,20 @@ impl types::PaymentsResponseData, > for Cashtocode { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Cashtocode".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index bacb707e5648..7bcf4e9f98ff 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -286,6 +286,20 @@ impl > for Checkout { // Issue: #173 + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Checkout".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 6753525ca474..76f88b663265 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -157,6 +157,20 @@ impl types::PaymentsResponseData, > for Coinbase { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Coinbase".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index 124e352bc06d..95ea7ef0c7a9 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -184,6 +184,20 @@ impl types::PaymentsResponseData, > for Cryptopay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Cryptopay".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/dlocal.rs b/crates/router/src/connector/dlocal.rs index 155d422d895b..1e6fc7561ed4 100644 --- a/crates/router/src/connector/dlocal.rs +++ b/crates/router/src/connector/dlocal.rs @@ -180,6 +180,20 @@ impl types::PaymentsResponseData, > for Dlocal { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Dlocal".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index 8f5f64e4da6b..17f161ad85bb 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -153,6 +153,20 @@ impl types::PaymentsResponseData, > for DummyConnector { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for DummyConnector".to_string(), + ) + .into()) + } } impl diff --git a/crates/router/src/connector/fiserv.rs b/crates/router/src/connector/fiserv.rs index 024ba8d11f06..0bd821156275 100644 --- a/crates/router/src/connector/fiserv.rs +++ b/crates/router/src/connector/fiserv.rs @@ -212,6 +212,20 @@ impl types::PaymentsResponseData, > for Fiserv { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Fiserv".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Fiserv {} diff --git a/crates/router/src/connector/forte.rs b/crates/router/src/connector/forte.rs index 2a9fb8159f23..f23907232c48 100644 --- a/crates/router/src/connector/forte.rs +++ b/crates/router/src/connector/forte.rs @@ -169,6 +169,20 @@ impl types::PaymentsResponseData, > for Forte { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Forte".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index 01ec761ae8cc..9c5cdd34509d 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -340,6 +340,20 @@ impl types::PaymentsResponseData, > for Globalpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Globalpay".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Globalpay {} diff --git a/crates/router/src/connector/globepay.rs b/crates/router/src/connector/globepay.rs index 9870077b9b13..8f8b01de8efe 100644 --- a/crates/router/src/connector/globepay.rs +++ b/crates/router/src/connector/globepay.rs @@ -150,6 +150,20 @@ impl types::PaymentsResponseData, > for Globepay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Globepay".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/helcim.rs b/crates/router/src/connector/helcim.rs index 160c972194f6..af2ed69c1caa 100644 --- a/crates/router/src/connector/helcim.rs +++ b/crates/router/src/connector/helcim.rs @@ -203,20 +203,25 @@ impl } fn build_request( &self, - req: &types::SetupMandateRouterData, - connectors: &settings::Connectors, + _req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::SetupMandateType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .set_body(types::SetupMandateType::get_request_body( - self, req, connectors, - )?) - .build(), - )) + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Helcim".to_string()) + .into(), + ) + + // Ok(Some( + // services::RequestBuilder::new() + // .method(services::Method::Post) + // .url(&types::SetupMandateType::get_url(self, req, connectors)?) + // .attach_default_headers() + // .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + // .set_body(types::SetupMandateType::get_request_body( + // self, req, connectors, + // )?) + // .build(), + // )) } fn handle_response( &self, diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 2fbd1b208a58..b7c87b5336b0 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -248,6 +248,20 @@ impl types::PaymentsResponseData, > for Iatapay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Iatapay".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index f2bab66b724b..5361a636826d 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -218,6 +218,20 @@ impl > for Klarna { // Not Implemented(R) + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Klarna".to_string()) + .into(), + ) + } } impl diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs index 023ab3da7acc..cee4225d861e 100644 --- a/crates/router/src/connector/mollie.rs +++ b/crates/router/src/connector/mollie.rs @@ -209,6 +209,20 @@ impl types::PaymentsResponseData, > for Mollie { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Mollie".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/multisafepay.rs b/crates/router/src/connector/multisafepay.rs index d10642e0e023..159b132fa092 100644 --- a/crates/router/src/connector/multisafepay.rs +++ b/crates/router/src/connector/multisafepay.rs @@ -130,6 +130,20 @@ impl types::PaymentsResponseData, > for Multisafepay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for Multisafepay".to_string(), + ) + .into()) + } } impl api::PaymentVoid for Multisafepay {} diff --git a/crates/router/src/connector/nexinets.rs b/crates/router/src/connector/nexinets.rs index df0b8cae942e..1f4a7f41f66b 100644 --- a/crates/router/src/connector/nexinets.rs +++ b/crates/router/src/connector/nexinets.rs @@ -169,6 +169,20 @@ impl types::PaymentsResponseData, > for Nexinets { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Nexinets".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index aecb103194f1..df903ddd8a3d 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -149,19 +149,21 @@ impl fn build_request( &self, - req: &types::SetupMandateRouterData, - connectors: &settings::Connectors, + _req: &types::SetupMandateRouterData, + _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Post) - .url(&types::SetupMandateType::get_url(self, req, connectors)?) - .headers(types::SetupMandateType::get_headers(self, req, connectors)?) - .set_body(types::SetupMandateType::get_request_body( - self, req, connectors, - )?) - .build(), - )) + Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Nmi".to_string()).into()) + + // Ok(Some( + // services::RequestBuilder::new() + // .method(services::Method::Post) + // .url(&types::SetupMandateType::get_url(self, req, connectors)?) + // .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + // .set_body(types::SetupMandateType::get_request_body( + // self, req, connectors, + // )?) + // .build(), + // )) } fn handle_response( diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 581c0ee95dc7..708d2695c736 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -183,6 +183,20 @@ impl types::PaymentsResponseData, > for Noon { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Noon".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 6ab0033118e8..35de293443e8 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -118,6 +118,20 @@ impl types::PaymentsResponseData, > for Nuvei { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Nuvei".to_string()) + .into(), + ) + } } impl diff --git a/crates/router/src/connector/opayo.rs b/crates/router/src/connector/opayo.rs index 9c2d5f9204fc..cfa50321d304 100644 --- a/crates/router/src/connector/opayo.rs +++ b/crates/router/src/connector/opayo.rs @@ -147,6 +147,20 @@ impl types::PaymentsResponseData, > for Opayo { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Opayo".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index f4528ce8b273..1eafa67fddae 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -146,6 +146,20 @@ impl types::PaymentsResponseData, > for Opennode { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Opennode".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs index 3c8cd0e6f940..442bbaca3128 100644 --- a/crates/router/src/connector/payeezy.rs +++ b/crates/router/src/connector/payeezy.rs @@ -153,6 +153,20 @@ impl types::PaymentsResponseData, > for Payeezy { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payeezy".to_string()) + .into(), + ) + } } impl api::PaymentToken for Payeezy {} diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index de0b1c0ac8bf..40f4c646e507 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -329,6 +329,20 @@ impl types::PaymentsResponseData, > for Payme { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payme".to_string()) + .into(), + ) + } } impl services::ConnectorRedirectResponse for Payme { diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 7a9799d415dd..c63e9b0daa49 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -386,6 +386,20 @@ impl types::PaymentsResponseData, > for Paypal { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Paypal".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/payu.rs b/crates/router/src/connector/payu.rs index 85f55e11adbe..c44b4d5d6ffa 100644 --- a/crates/router/src/connector/payu.rs +++ b/crates/router/src/connector/payu.rs @@ -128,6 +128,20 @@ impl types::PaymentsResponseData, > for Payu { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payu".to_string()) + .into(), + ) + } } impl api::PaymentToken for Payu {} diff --git a/crates/router/src/connector/placetopay.rs b/crates/router/src/connector/placetopay.rs index 82998e97a06a..ef51f1d6e4ba 100644 --- a/crates/router/src/connector/placetopay.rs +++ b/crates/router/src/connector/placetopay.rs @@ -141,6 +141,20 @@ impl types::PaymentsResponseData, > for Placetopay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Placetopay".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/powertranz.rs b/crates/router/src/connector/powertranz.rs index ee82b931755d..654128bb3fd2 100644 --- a/crates/router/src/connector/powertranz.rs +++ b/crates/router/src/connector/powertranz.rs @@ -158,6 +158,20 @@ impl types::PaymentsResponseData, > for Powertranz { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Powertranz".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/prophetpay.rs b/crates/router/src/connector/prophetpay.rs index 24be540d411e..fc14f37a2b2d 100644 --- a/crates/router/src/connector/prophetpay.rs +++ b/crates/router/src/connector/prophetpay.rs @@ -145,6 +145,20 @@ impl types::PaymentsResponseData, > for Prophetpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Prophetpay".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 7927da431651..5272a8ab21e8 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -272,6 +272,20 @@ impl types::PaymentsResponseData, > for Rapyd { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Rapyd".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Rapyd {} diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index d4c24175dc41..29d57ee28cd9 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -14,7 +14,7 @@ use crate::{ configs::settings, core::errors::{self, CustomResult}, headers, - services::{request, ConnectorIntegration, ConnectorValidation}, + services::{self, request, ConnectorIntegration, ConnectorValidation}, types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, @@ -22,7 +22,6 @@ use crate::{ }; #[cfg(feature = "frm")] use crate::{ - services, types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, utils::BytesExt, }; @@ -460,6 +459,20 @@ impl types::PaymentsResponseData, > for Riskified { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Riskified".to_string()) + .into(), + ) + } } impl api::PaymentSession for Riskified {} diff --git a/crates/router/src/connector/shift4.rs b/crates/router/src/connector/shift4.rs index f1f00fd4b857..423a143604ca 100644 --- a/crates/router/src/connector/shift4.rs +++ b/crates/router/src/connector/shift4.rs @@ -158,6 +158,20 @@ impl types::PaymentsResponseData, > for Shift4 { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Shift4".to_string()) + .into(), + ) + } } #[async_trait::async_trait] diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs index a875166c54bf..544dfd137db4 100644 --- a/crates/router/src/connector/signifyd.rs +++ b/crates/router/src/connector/signifyd.rs @@ -11,7 +11,7 @@ use crate::{ configs::settings, core::errors::{self, CustomResult}, headers, - services::{request, ConnectorIntegration, ConnectorValidation}, + services::{self, request, ConnectorIntegration, ConnectorValidation}, types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, @@ -19,7 +19,6 @@ use crate::{ }; #[cfg(feature = "frm")] use crate::{ - services, types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, utils::BytesExt, }; @@ -127,6 +126,20 @@ impl types::PaymentsResponseData, > for Signifyd { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Signifyd".to_string()) + .into(), + ) + } } impl api::PaymentSession for Signifyd {} diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index f9da19776e45..e210a851dfc9 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -162,6 +162,20 @@ impl types::PaymentsResponseData, > for Square { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Square".to_string()) + .into(), + ) + } } #[async_trait::async_trait] diff --git a/crates/router/src/connector/stax.rs b/crates/router/src/connector/stax.rs index 7290b97562bc..75bb8e68184a 100644 --- a/crates/router/src/connector/stax.rs +++ b/crates/router/src/connector/stax.rs @@ -320,6 +320,20 @@ impl types::PaymentsResponseData, > for Stax { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Stax".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index ef8099159644..61ee2e2659d9 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -178,6 +178,20 @@ impl types::PaymentsResponseData, > for Trustpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Trustpay".to_string()) + .into(), + ) + } } impl api::PaymentVoid for Trustpay {} diff --git a/crates/router/src/connector/tsys.rs b/crates/router/src/connector/tsys.rs index d765729fade3..8c19f9f823ae 100644 --- a/crates/router/src/connector/tsys.rs +++ b/crates/router/src/connector/tsys.rs @@ -113,6 +113,20 @@ impl types::PaymentsResponseData, > for Tsys { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Tsys".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 6910d88cfb00..f239f5980145 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -234,6 +234,20 @@ impl types::PaymentsResponseData, > for Volt { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Volt".to_string()) + .into(), + ) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index d41b44aaec5e..0674cc2ff8bc 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -157,6 +157,20 @@ impl types::PaymentsResponseData, > for Wise { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Wise".to_string()) + .into(), + ) + } } impl api::PaymentSession for Wise {} diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index c9e0c05eadf6..a1ca8a110bc1 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -173,6 +173,20 @@ impl types::PaymentsResponseData, > for Worldline { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldline".to_string()) + .into(), + ) + } } impl api::PaymentToken for Worldline {} diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index 6e898f531da4..1684ddb911de 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -125,6 +125,20 @@ impl types::PaymentsResponseData, > for Worldpay { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Worldpay".to_string()) + .into(), + ) + } } impl api::PaymentToken for Worldpay {} diff --git a/crates/router/src/connector/zen.rs b/crates/router/src/connector/zen.rs index 439027ac44ad..493efb297243 100644 --- a/crates/router/src/connector/zen.rs +++ b/crates/router/src/connector/zen.rs @@ -170,6 +170,17 @@ impl types::PaymentsResponseData, > for Zen { + fn build_request( + &self, + _req: &types::RouterData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Setup Mandate flow for Zen".to_string()).into()) + } } impl ConnectorIntegration From 30c14019d067ad5f105563f205eb1941010233e8 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 19 Dec 2023 20:11:31 +0530 Subject: [PATCH 231/443] feat(connector): [NMI] Implement webhook for Payments and Refunds (#3164) --- crates/router/src/connector/nmi.rs | 124 +++++++++++- .../router/src/connector/nmi/transformers.rs | 177 +++++++++++++++--- 2 files changed, 272 insertions(+), 29 deletions(-) diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index df903ddd8a3d..0c01c752039f 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -2,9 +2,10 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; +use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; +use regex::Regex; use transformers as nmi; use super::utils as connector_utils; @@ -15,6 +16,7 @@ use crate::{ types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, + transformers::ForeignFrom, ErrorResponse, }, }; @@ -94,6 +96,14 @@ impl ConnectorValidation for Nmi { ), } } + + fn validate_psync_reference_id( + &self, + _data: &types::PaymentsSyncRouterData, + ) -> CustomResult<(), errors::ConnectorError> { + // in case we dont have transaction id, we can make psync using attempt id + Ok(()) + } } impl @@ -784,24 +794,124 @@ impl ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let sig_header = + connector_utils::get_header_key_value("webhook-signature", request.headers)?; + + let regex_pattern = r"t=(.*),s=(.*)"; + + if let Some(captures) = Regex::new(regex_pattern) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)? + .captures(sig_header) + { + let signature = captures + .get(1) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()? + .as_str(); + return Ok(signature.as_bytes().to_vec()); + } + + Err(errors::ConnectorError::WebhookSignatureNotFound).into_report() + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let sig_header = + connector_utils::get_header_key_value("webhook-signature", request.headers)?; + + let regex_pattern = r"t=(.*),s=(.*)"; + + if let Some(captures) = Regex::new(regex_pattern) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound)? + .captures(sig_header) + { + let nonce = captures + .get(0) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound) + .into_report()? + .as_str(); + + let message = format!("{}.{}", nonce, String::from_utf8_lossy(request.body)); + + return Ok(message.into_bytes()); + } + Err(errors::ConnectorError::WebhookSignatureNotFound).into_report() + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let reference_body: nmi::NmiWebhookObjectReference = request + .body + .parse_struct("nmi NmiWebhookObjectReference") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + let object_reference_id = match reference_body.event_body.action.action_type { + nmi::NmiActionType::Sale => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Refund => api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::RefundId(reference_body.event_body.order_id), + ), + _ => Err(errors::ConnectorError::WebhooksNotImplemented).into_report()?, + }; + + Ok(object_reference_id) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Ok(api::IncomingWebhookEvent::EventNotSupported) + let event_type_body: nmi::NmiWebhookEventBody = request + .body + .parse_struct("nmi NmiWebhookEventType") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(api::IncomingWebhookEvent::foreign_from( + event_type_body.event_type, + )) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let webhook_body: nmi::NmiWebhookBody = request + .body + .parse_struct("nmi NmiWebhookBody") + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + match webhook_body.event_body.action.action_type { + nmi::NmiActionType::Sale + | nmi::NmiActionType::Auth + | nmi::NmiActionType::Capture + | nmi::NmiActionType::Void + | nmi::NmiActionType::Credit => { + Ok(Box::new(nmi::SyncResponse::try_from(&webhook_body)?)) + } + nmi::NmiActionType::Refund => Ok(Box::new(webhook_body)), + } } } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 5dfcdcf8b99d..6146f4a45992 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -1,3 +1,4 @@ +use api_models::webhooks; use cards::CardNumber; use common_utils::{errors::CustomResult, ext_traits::XmlExt}; use error_stack::{IntoReport, Report, ResultExt}; @@ -319,9 +320,7 @@ impl let (response, status) = match item.response.response { Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, - ), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.orderid), redirection_data: None, mandate_reference: None, connector_metadata: None, @@ -544,7 +543,7 @@ impl TryFrom<&types::SetupMandateRouterData> for NmiPaymentsRequest { #[derive(Debug, Serialize)] pub struct NmiSyncRequest { - pub transaction_id: String, + pub order_id: String, pub security_key: Secret, } @@ -554,11 +553,7 @@ impl TryFrom<&types::PaymentsSyncRouterData> for NmiSyncRequest { let auth = NmiAuthType::try_from(&item.connector_auth_type)?; Ok(Self { security_key: auth.api_key, - transaction_id: item - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, + order_id: item.attempt_id.clone(), }) } } @@ -889,6 +884,19 @@ impl TryFrom> for SyncResponse { } } +impl TryFrom> for NmiRefundSyncResponse { + type Error = Error; + fn try_from(bytes: Vec) -> Result { + let query_response = String::from_utf8(bytes) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + query_response + .parse_xml::() + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + } +} + impl From for enums::AttemptStatus { fn from(item: NmiStatus) -> Self { match item { @@ -909,6 +917,7 @@ pub struct NmiRefundRequest { transaction_type: TransactionType, security_key: Secret, transactionid: String, + orderid: String, amount: f64, } @@ -920,6 +929,7 @@ impl TryFrom<&NmiRouterData<&types::RefundsRouterData>> for NmiRefundReque transaction_type: TransactionType::Refund, security_key: auth_type.api_key, transactionid: item.router_data.request.connector_transaction_id.clone(), + orderid: item.router_data.request.refund_id.clone(), amount: item.amount, }) } @@ -935,7 +945,7 @@ impl TryFrom> let refund_status = enums::RefundStatus::from(item.response.response); Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.transactionid, + connector_refund_id: item.response.orderid, refund_status, }), ..item.data @@ -974,15 +984,14 @@ impl TryFrom<&types::RefundSyncRouterData> for NmiSyncRequest { type Error = Error; fn try_from(item: &types::RefundSyncRouterData) -> Result { let auth = NmiAuthType::try_from(&item.connector_auth_type)?; - let transaction_id = item - .request - .connector_refund_id - .clone() - .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; Ok(Self { security_key: auth.api_key, - transaction_id, + order_id: item + .request + .connector_refund_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorRefundID)?, }) } } @@ -994,12 +1003,12 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let response = SyncResponse::try_from(item.response.response.to_vec())?; + let response = NmiRefundSyncResponse::try_from(item.response.response.to_vec())?; let refund_status = enums::RefundStatus::from(NmiStatus::from(response.transaction.condition)); Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: response.transaction.transaction_id, + connector_refund_id: response.transaction.order_id, refund_status, }), ..item.data @@ -1036,13 +1045,137 @@ impl From for NmiStatus { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct SyncTransactionResponse { - transaction_id: String, + pub transaction_id: String, + pub condition: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SyncResponse { + pub transaction: SyncTransactionResponse, +} + +#[derive(Debug, Deserialize)] +pub struct RefundSyncBody { + order_id: String, condition: String, } #[derive(Debug, Deserialize)] -struct SyncResponse { - transaction: SyncTransactionResponse, +struct NmiRefundSyncResponse { + transaction: RefundSyncBody, +} + +#[derive(Debug, Deserialize)] +pub struct NmiWebhookObjectReference { + pub event_body: NmiReferenceBody, +} + +#[derive(Debug, Deserialize)] +pub struct NmiReferenceBody { + pub order_id: String, + pub action: NmiActionBody, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NmiActionBody { + pub action_type: NmiActionType, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum NmiActionType { + Auth, + Capture, + Credit, + Refund, + Sale, + Void, +} + +#[derive(Debug, Deserialize)] +pub struct NmiWebhookEventBody { + pub event_type: NmiWebhookEventType, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum NmiWebhookEventType { + #[serde(rename = "transaction.sale.success")] + SaleSuccess, + #[serde(rename = "transaction.sale.failure")] + SaleFailure, + #[serde(rename = "transaction.sale.unknown")] + SaleUnknown, + #[serde(rename = "transaction.auth.success")] + AuthSuccess, + #[serde(rename = "transaction.auth.failure")] + AuthFailure, + #[serde(rename = "transaction.auth.unknown")] + AuthUnknown, + #[serde(rename = "transaction.refund.success")] + RefundSuccess, + #[serde(rename = "transaction.refund.failure")] + RefundFailure, + #[serde(rename = "transaction.refund.unknown")] + RefundUnknown, + #[serde(rename = "transaction.void.success")] + VoidSuccess, + #[serde(rename = "transaction.void.failure")] + VoidFailure, + #[serde(rename = "transaction.void.unknown")] + VoidUnknown, + #[serde(rename = "transaction.capture.success")] + CaptureSuccess, + #[serde(rename = "transaction.capture.failure")] + CaptureFailure, + #[serde(rename = "transaction.capture.unknown")] + CaptureUnknown, +} + +impl ForeignFrom for webhooks::IncomingWebhookEvent { + fn foreign_from(status: NmiWebhookEventType) -> Self { + match status { + NmiWebhookEventType::SaleSuccess => Self::PaymentIntentSuccess, + NmiWebhookEventType::SaleFailure => Self::PaymentIntentFailure, + NmiWebhookEventType::RefundSuccess => Self::RefundSuccess, + NmiWebhookEventType::RefundFailure => Self::RefundFailure, + NmiWebhookEventType::VoidSuccess => Self::PaymentIntentCancelled, + NmiWebhookEventType::SaleUnknown + | NmiWebhookEventType::RefundUnknown + | NmiWebhookEventType::AuthSuccess + | NmiWebhookEventType::AuthFailure + | NmiWebhookEventType::AuthUnknown + | NmiWebhookEventType::VoidFailure + | NmiWebhookEventType::VoidUnknown + | NmiWebhookEventType::CaptureSuccess + | NmiWebhookEventType::CaptureFailure + | NmiWebhookEventType::CaptureUnknown => Self::EventNotSupported, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NmiWebhookBody { + pub event_body: NmiWebhookObject, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NmiWebhookObject { + pub transaction_id: String, + pub order_id: String, + pub condition: String, + pub action: NmiActionBody, +} + +impl TryFrom<&NmiWebhookBody> for SyncResponse { + type Error = Error; + fn try_from(item: &NmiWebhookBody) -> Result { + let transaction = SyncTransactionResponse { + transaction_id: item.event_body.transaction_id.to_owned(), + condition: item.event_body.condition.to_owned(), + }; + + Ok(Self { transaction }) + } } From b0ffbe9355b7e38226994c1ccbbe80cdbc77adde Mon Sep 17 00:00:00 2001 From: Arjun Karthik Date: Tue, 19 Dec 2023 22:52:06 +0530 Subject: [PATCH 232/443] feat(connector-config): add wasm support for dashboard connector configuration (#3138) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- Cargo.lock | 12 + crates/connector_configs/Cargo.toml | 22 + crates/connector_configs/src/common_config.rs | 133 +++ crates/connector_configs/src/connector.rs | 267 ++++++ crates/connector_configs/src/lib.rs | 4 + .../src/response_modifier.rs | 313 +++++++ crates/connector_configs/src/transformer.rs | 284 +++++++ .../connector_configs/toml/development.toml | 789 ++++++++++++++++++ crates/connector_configs/toml/production.toml | 614 ++++++++++++++ crates/connector_configs/toml/sandbox.toml | 786 +++++++++++++++++ crates/euclid_wasm/Cargo.toml | 6 +- crates/euclid_wasm/src/lib.rs | 40 +- docker/wasm-build.Dockerfile | 25 + 13 files changed, 3292 insertions(+), 3 deletions(-) create mode 100644 crates/connector_configs/Cargo.toml create mode 100644 crates/connector_configs/src/common_config.rs create mode 100644 crates/connector_configs/src/connector.rs create mode 100644 crates/connector_configs/src/lib.rs create mode 100644 crates/connector_configs/src/response_modifier.rs create mode 100644 crates/connector_configs/src/transformer.rs create mode 100644 crates/connector_configs/toml/development.toml create mode 100644 crates/connector_configs/toml/production.toml create mode 100644 crates/connector_configs/toml/sandbox.toml create mode 100644 docker/wasm-build.Dockerfile diff --git a/Cargo.lock b/Cargo.lock index f15651e7c350..be1e0c8a2a4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1727,6 +1727,17 @@ dependencies = [ "toml 0.7.4", ] +[[package]] +name = "connector_configs" +version = "0.1.0" +dependencies = [ + "api_models", + "serde", + "serde_with", + "toml 0.7.4", + "utoipa", +] + [[package]] name = "constant_time_eq" version = "0.2.6" @@ -2386,6 +2397,7 @@ version = "0.1.0" dependencies = [ "api_models", "common_enums", + "connector_configs", "currency_conversion", "euclid", "getrandom 0.2.11", diff --git a/crates/connector_configs/Cargo.toml b/crates/connector_configs/Cargo.toml new file mode 100644 index 000000000000..f4df1e6a20b3 --- /dev/null +++ b/crates/connector_configs/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "connector_configs" +description = "Connector Integration Dashboard" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[features] +default = ["payouts", "dummy_connector"] +production = [] +development = [] +sandbox = [] +dummy_connector = ["api_models/dummy_connector", "development"] +payouts = [] + +[dependencies] +api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } +serde = { version = "1.0.193", features = ["derive"] } +serde_with = "3.4.0" +toml = "0.7.3" +utoipa = { version = "3.3.0", features = ["preserve_order"] } diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs new file mode 100644 index 000000000000..6ba44d4ed7eb --- /dev/null +++ b/crates/connector_configs/src/common_config.rs @@ -0,0 +1,133 @@ +use api_models::{payment_methods, payments}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct ZenApplePay { + pub terminal_uuid: Option, + pub pay_wall_secret: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum ApplePayData { + ApplePay(payments::ApplePayMetadata), + ApplePayCombined(payments::ApplePayCombinedMetadata), + Zen(ZenApplePay), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GpayDashboardPayLoad { + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway_merchant_id: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "stripe:version")] + pub stripe_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename( + serialize = "stripe_publishable_key", + deserialize = "stripe:publishable_key" + ))] + #[serde(alias = "stripe:publishable_key")] + #[serde(alias = "stripe_publishable_key")] + pub stripe_publishable_key: Option, + pub merchant_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_id: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct ZenGooglePay { + pub terminal_uuid: Option, + pub pay_wall_secret: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum GooglePayData { + Standard(GpayDashboardPayLoad), + Zen(ZenGooglePay), +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum GoogleApiModelData { + Standard(payments::GpayMetaData), + Zen(ZenGooglePay), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct PaymentMethodsEnabled { + pub payment_method: api_models::enums::PaymentMethod, + pub payment_method_types: Option>, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct ApiModelMetaData { + pub merchant_config_currency: Option, + pub merchant_account_id: Option, + pub account_name: Option, + pub terminal_id: Option, + pub merchant_id: Option, + pub google_pay: Option, + pub apple_pay: Option, + pub apple_pay_combined: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct ConnectorApiIntegrationPayload { + pub connector_type: String, + pub profile_id: String, + pub connector_name: api_models::enums::Connector, + #[serde(skip_deserializing)] + #[schema(example = "stripe_US_travel")] + pub connector_label: Option, + pub merchant_connector_id: Option, + pub disabled: bool, + pub test_mode: bool, + pub payment_methods_enabled: Option>, + pub metadata: Option, + pub connector_webhook_details: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct DashboardPaymentMethodPayload { + pub payment_method: api_models::enums::PaymentMethod, + pub payment_method_type: String, + pub provider: Option>, + pub card_provider: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct DashboardRequestPayload { + pub connector: api_models::enums::Connector, + pub payment_methods_enabled: Option>, + pub metadata: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct DashboardMetaData { + pub merchant_config_currency: Option, + pub merchant_account_id: Option, + pub account_name: Option, + pub terminal_id: Option, + pub merchant_id: Option, + pub google_pay: Option, + pub apple_pay: Option, + pub apple_pay_combined: Option, +} diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs new file mode 100644 index 000000000000..f41fa4aab457 --- /dev/null +++ b/crates/connector_configs/src/connector.rs @@ -0,0 +1,267 @@ +use std::collections::HashMap; + +#[cfg(feature = "payouts")] +use api_models::enums::PayoutConnectors; +use api_models::{ + enums::{CardNetwork, Connector, PaymentMethodType}, + payments, +}; +use serde::Deserialize; +#[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] +use toml; + +use crate::common_config::{GooglePayData, ZenApplePay}; + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CurrencyAuthKeyType { + pub password_classic: Option, + pub username_classic: Option, + pub merchant_id_classic: Option, + pub password_evoucher: Option, + pub username_evoucher: Option, + pub merchant_id_evoucher: Option, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ConnectorAuthType { + HeaderKey { + api_key: String, + }, + BodyKey { + api_key: String, + key1: String, + }, + SignatureKey { + api_key: String, + key1: String, + api_secret: String, + }, + MultiAuthKey { + api_key: String, + key1: String, + api_secret: String, + key2: String, + }, + CurrencyAuthKey { + auth_key_map: HashMap, + }, + #[default] + NoKey, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +#[serde(untagged)] +pub enum ApplePayTomlConfig { + Standard(payments::ApplePayMetadata), + Zen(ZenApplePay), +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct ConfigMetadata { + pub merchant_config_currency: Option, + pub merchant_account_id: Option, + pub account_name: Option, + pub terminal_id: Option, + pub google_pay: Option, + pub apple_pay: Option, + pub merchant_id: Option, +} + +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct ConnectorTomlConfig { + pub connector_auth: Option, + pub connector_webhook_details: Option, + pub metadata: Option, + pub credit: Option>, + pub debit: Option>, + pub bank_transfer: Option>, + pub bank_redirect: Option>, + pub bank_debit: Option>, + pub pay_later: Option>, + pub wallet: Option>, + pub crypto: Option>, + pub reward: Option>, + pub upi: Option>, + pub voucher: Option>, + pub gift_card: Option>, + pub card_redirect: Option>, + pub is_verifiable: Option, +} +#[serde_with::skip_serializing_none] +#[derive(Debug, Deserialize, serde::Serialize, Clone)] +pub struct ConnectorConfig { + pub aci: Option, + pub adyen: Option, + #[cfg(feature = "payouts")] + pub adyen_payout: Option, + pub airwallex: Option, + pub authorizedotnet: Option, + pub bankofamerica: Option, + pub bitpay: Option, + pub bluesnap: Option, + pub boku: Option, + pub braintree: Option, + pub cashtocode: Option, + pub checkout: Option, + pub coinbase: Option, + pub cryptopay: Option, + pub cybersource: Option, + pub iatapay: Option, + pub opennode: Option, + pub bambora: Option, + pub dlocal: Option, + pub fiserv: Option, + pub forte: Option, + pub globalpay: Option, + pub globepay: Option, + pub gocardless: Option, + pub helcim: Option, + pub klarna: Option, + pub mollie: Option, + pub multisafepay: Option, + pub nexinets: Option, + pub nmi: Option, + pub noon: Option, + pub nuvei: Option, + pub payme: Option, + pub paypal: Option, + pub payu: Option, + pub placetopay: Option, + pub plaid: Option, + pub powertranz: Option, + pub prophetpay: Option, + pub riskified: Option, + pub rapyd: Option, + pub shift4: Option, + pub stripe: Option, + pub signifyd: Option, + pub trustpay: Option, + pub tsys: Option, + pub volt: Option, + #[cfg(feature = "payouts")] + pub wise_payout: Option, + pub worldline: Option, + pub worldpay: Option, + pub zen: Option, + pub square: Option, + pub stax: Option, + pub dummy_connector: Option, + pub stripe_test: Option, + pub paypal_test: Option, +} + +impl ConnectorConfig { + fn new() -> Result { + #[cfg(all( + feature = "production", + not(any(feature = "sandbox", feature = "development")) + ))] + let config = toml::from_str::(include_str!("../toml/production.toml")); + #[cfg(all( + feature = "sandbox", + not(any(feature = "production", feature = "development")) + ))] + let config = toml::from_str::(include_str!("../toml/sandbox.toml")); + #[cfg(feature = "development")] + let config = toml::from_str::(include_str!("../toml/development.toml")); + + #[cfg(not(any(feature = "sandbox", feature = "development", feature = "production")))] + return Err(String::from( + "Atleast one features has to be enabled for connectorconfig", + )); + + #[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] + match config { + Ok(data) => Ok(data), + Err(err) => Err(err.to_string()), + } + } + + #[cfg(feature = "payouts")] + pub fn get_payout_connector_config( + connector: PayoutConnectors, + ) -> Result, String> { + let connector_data = Self::new()?; + match connector { + PayoutConnectors::Adyen => Ok(connector_data.adyen_payout), + PayoutConnectors::Wise => Ok(connector_data.wise_payout), + } + } + + pub fn get_connector_config( + connector: Connector, + ) -> Result, String> { + let connector_data = Self::new()?; + match connector { + Connector::Aci => Ok(connector_data.aci), + Connector::Adyen => Ok(connector_data.adyen), + Connector::Airwallex => Ok(connector_data.airwallex), + Connector::Authorizedotnet => Ok(connector_data.authorizedotnet), + Connector::Bankofamerica => Ok(connector_data.bankofamerica), + Connector::Bitpay => Ok(connector_data.bitpay), + Connector::Bluesnap => Ok(connector_data.bluesnap), + Connector::Boku => Ok(connector_data.boku), + Connector::Braintree => Ok(connector_data.braintree), + Connector::Cashtocode => Ok(connector_data.cashtocode), + Connector::Checkout => Ok(connector_data.checkout), + Connector::Coinbase => Ok(connector_data.coinbase), + Connector::Cryptopay => Ok(connector_data.cryptopay), + Connector::Cybersource => Ok(connector_data.cybersource), + Connector::Iatapay => Ok(connector_data.iatapay), + Connector::Opennode => Ok(connector_data.opennode), + Connector::Bambora => Ok(connector_data.bambora), + Connector::Dlocal => Ok(connector_data.dlocal), + Connector::Fiserv => Ok(connector_data.fiserv), + Connector::Forte => Ok(connector_data.forte), + Connector::Globalpay => Ok(connector_data.globalpay), + Connector::Globepay => Ok(connector_data.globepay), + Connector::Gocardless => Ok(connector_data.gocardless), + Connector::Helcim => Ok(connector_data.helcim), + Connector::Klarna => Ok(connector_data.klarna), + Connector::Mollie => Ok(connector_data.mollie), + Connector::Multisafepay => Ok(connector_data.multisafepay), + Connector::Nexinets => Ok(connector_data.nexinets), + Connector::Prophetpay => Ok(connector_data.prophetpay), + Connector::Nmi => Ok(connector_data.nmi), + Connector::Noon => Ok(connector_data.noon), + Connector::Nuvei => Ok(connector_data.nuvei), + Connector::Payme => Ok(connector_data.payme), + Connector::Paypal => Ok(connector_data.paypal), + Connector::Payu => Ok(connector_data.payu), + Connector::Placetopay => Ok(connector_data.placetopay), + Connector::Plaid => Ok(connector_data.plaid), + Connector::Powertranz => Ok(connector_data.powertranz), + Connector::Rapyd => Ok(connector_data.rapyd), + Connector::Riskified => Ok(connector_data.riskified), + Connector::Shift4 => Ok(connector_data.shift4), + Connector::Signifyd => Ok(connector_data.signifyd), + Connector::Square => Ok(connector_data.square), + Connector::Stax => Ok(connector_data.stax), + Connector::Stripe => Ok(connector_data.stripe), + Connector::Trustpay => Ok(connector_data.trustpay), + Connector::Tsys => Ok(connector_data.tsys), + Connector::Volt => Ok(connector_data.volt), + Connector::Wise => Err("Use get_payout_connector_config".to_string()), + Connector::Worldline => Ok(connector_data.worldline), + Connector::Worldpay => Ok(connector_data.worldpay), + Connector::Zen => Ok(connector_data.zen), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector1 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector2 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector3 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector4 => Ok(connector_data.stripe_test), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector5 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector6 => Ok(connector_data.dummy_connector), + #[cfg(feature = "dummy_connector")] + Connector::DummyConnector7 => Ok(connector_data.paypal_test), + } + } +} diff --git a/crates/connector_configs/src/lib.rs b/crates/connector_configs/src/lib.rs new file mode 100644 index 000000000000..d480871c747f --- /dev/null +++ b/crates/connector_configs/src/lib.rs @@ -0,0 +1,4 @@ +pub mod common_config; +pub mod connector; +pub mod response_modifier; +pub mod transformer; diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs new file mode 100644 index 000000000000..0eb447ace1a9 --- /dev/null +++ b/crates/connector_configs/src/response_modifier.rs @@ -0,0 +1,313 @@ +use crate::common_config::{ + ConnectorApiIntegrationPayload, DashboardMetaData, DashboardPaymentMethodPayload, + DashboardRequestPayload, GoogleApiModelData, GooglePayData, GpayDashboardPayLoad, +}; + +impl ConnectorApiIntegrationPayload { + pub fn get_transformed_response_payload(response: Self) -> DashboardRequestPayload { + let mut wallet_details = Vec::new(); + let mut bank_redirect_details = Vec::new(); + let mut pay_later_details = Vec::new(); + let mut debit_details = Vec::new(); + let mut credit_details = Vec::new(); + let mut bank_transfer_details = Vec::new(); + let mut crypto_details = Vec::new(); + let mut bank_debit_details = Vec::new(); + let mut reward_details = Vec::new(); + let mut upi_details = Vec::new(); + let mut voucher_details = Vec::new(); + let mut gift_card_details = Vec::new(); + let mut card_redirect_details = Vec::new(); + + if let Some(payment_methods_enabled) = response.payment_methods_enabled.clone() { + for methods in payment_methods_enabled { + match methods.payment_method { + api_models::enums::PaymentMethod::Card => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + let payment_type = method_type.payment_method_type; + match payment_type { + api_models::enums::PaymentMethodType::Credit => { + if let Some(card_networks) = method_type.card_networks { + for card in card_networks { + credit_details.push(card) + } + } + } + api_models::enums::PaymentMethodType::Debit => { + if let Some(card_networks) = method_type.card_networks { + for card in card_networks { + debit_details.push(card) + } + } + } + _ => (), + } + } + } + } + api_models::enums::PaymentMethod::Wallet => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + wallet_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::BankRedirect => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + bank_redirect_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::PayLater => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + pay_later_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::BankTransfer => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + bank_transfer_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::Crypto => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + crypto_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::BankDebit => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + bank_debit_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::Reward => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + reward_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::Upi => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + upi_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::Voucher => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + voucher_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::GiftCard => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + gift_card_details.push(method_type.payment_method_type) + } + } + } + api_models::enums::PaymentMethod::CardRedirect => { + if let Some(payment_method_types) = methods.payment_method_types { + for method_type in payment_method_types { + card_redirect_details.push(method_type.payment_method_type) + } + } + } + } + } + } + + let upi = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Upi, + payment_method_type: api_models::enums::PaymentMethod::Upi.to_string(), + provider: Some(upi_details), + card_provider: None, + }; + + let voucher: DashboardPaymentMethodPayload = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Voucher, + payment_method_type: api_models::enums::PaymentMethod::Voucher.to_string(), + provider: Some(voucher_details), + card_provider: None, + }; + + let gift_card: DashboardPaymentMethodPayload = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::GiftCard, + payment_method_type: api_models::enums::PaymentMethod::GiftCard.to_string(), + provider: Some(gift_card_details), + card_provider: None, + }; + + let reward = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Reward, + payment_method_type: api_models::enums::PaymentMethod::Reward.to_string(), + provider: Some(reward_details), + card_provider: None, + }; + + let wallet = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Wallet, + payment_method_type: api_models::enums::PaymentMethod::Wallet.to_string(), + provider: Some(wallet_details), + card_provider: None, + }; + let bank_redirect = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::BankRedirect, + payment_method_type: api_models::enums::PaymentMethod::BankRedirect.to_string(), + provider: Some(bank_redirect_details), + card_provider: None, + }; + + let bank_debit = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::BankDebit, + payment_method_type: api_models::enums::PaymentMethod::BankDebit.to_string(), + provider: Some(bank_debit_details), + card_provider: None, + }; + + let bank_transfer = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::BankTransfer, + payment_method_type: api_models::enums::PaymentMethod::BankTransfer.to_string(), + provider: Some(bank_transfer_details), + card_provider: None, + }; + + let crypto = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Crypto, + payment_method_type: api_models::enums::PaymentMethod::Crypto.to_string(), + provider: Some(crypto_details), + card_provider: None, + }; + + let card_redirect = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::CardRedirect, + payment_method_type: api_models::enums::PaymentMethod::CardRedirect.to_string(), + provider: Some(card_redirect_details), + card_provider: None, + }; + let pay_later = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::PayLater, + payment_method_type: api_models::enums::PaymentMethod::PayLater.to_string(), + provider: Some(pay_later_details), + card_provider: None, + }; + let debit_details = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Card, + payment_method_type: api_models::enums::PaymentMethodType::Debit.to_string(), + provider: None, + card_provider: Some(debit_details), + }; + let credit_details = DashboardPaymentMethodPayload { + payment_method: api_models::enums::PaymentMethod::Card, + payment_method_type: api_models::enums::PaymentMethodType::Credit.to_string(), + provider: None, + card_provider: Some(credit_details), + }; + + let google_pay = Self::get_google_pay_metadata_response(response.clone()); + let account_name = match response.metadata.clone() { + Some(meta_data) => meta_data.account_name, + _ => None, + }; + + let merchant_account_id = match response.metadata.clone() { + Some(meta_data) => meta_data.merchant_account_id, + _ => None, + }; + let merchant_id = match response.metadata.clone() { + Some(meta_data) => meta_data.merchant_id, + _ => None, + }; + let terminal_id = match response.metadata.clone() { + Some(meta_data) => meta_data.terminal_id, + _ => None, + }; + let apple_pay = match response.metadata.clone() { + Some(meta_data) => meta_data.apple_pay, + _ => None, + }; + let apple_pay_combined = match response.metadata.clone() { + Some(meta_data) => meta_data.apple_pay_combined, + _ => None, + }; + let merchant_config_currency = match response.metadata.clone() { + Some(meta_data) => meta_data.merchant_config_currency, + _ => None, + }; + + let meta_data = DashboardMetaData { + merchant_config_currency, + merchant_account_id, + apple_pay, + apple_pay_combined, + google_pay, + account_name, + terminal_id, + merchant_id, + }; + + DashboardRequestPayload { + connector: response.connector_name, + payment_methods_enabled: Some(vec![ + upi, + voucher, + reward, + wallet, + bank_redirect, + bank_debit, + bank_transfer, + crypto, + card_redirect, + pay_later, + debit_details, + credit_details, + gift_card, + ]), + metadata: Some(meta_data), + } + } + + pub fn get_google_pay_metadata_response(response: Self) -> Option { + match response.metadata { + Some(meta_data) => match meta_data.google_pay { + Some(google_pay) => match google_pay { + GoogleApiModelData::Standard(standard_data) => { + if standard_data.allowed_payment_methods.is_empty() { + None + } else { + let data = Some( + standard_data.allowed_payment_methods[0] + .tokenization_specification + .parameters + .clone(), + ); + match data { + Some(data) => Some(GooglePayData::Standard(GpayDashboardPayLoad { + gateway_merchant_id: data.gateway_merchant_id, + stripe_version: data.stripe_version, + stripe_publishable_key: data.stripe_publishable_key, + merchant_name: standard_data.merchant_info.merchant_name, + merchant_id: standard_data.merchant_info.merchant_id, + })), + None => None, + } + } + } + GoogleApiModelData::Zen(data) => Some(GooglePayData::Zen(data)), + }, + None => None, + }, + None => None, + } + } +} diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs new file mode 100644 index 000000000000..aff75128e9cf --- /dev/null +++ b/crates/connector_configs/src/transformer.rs @@ -0,0 +1,284 @@ +use std::str::FromStr; + +use api_models::{ + enums::{ + Connector, PaymentMethod, PaymentMethodType, + PaymentMethodType::{AliPay, ApplePay, GooglePay, Klarna, Paypal, WeChatPay}, + }, + payment_methods, payments, +}; + +use crate::common_config::{ + ApiModelMetaData, ConnectorApiIntegrationPayload, DashboardMetaData, DashboardRequestPayload, + GoogleApiModelData, GooglePayData, PaymentMethodsEnabled, +}; + +impl DashboardRequestPayload { + pub fn transform_card( + payment_method_type: PaymentMethodType, + card_provider: Vec, + ) -> payment_methods::RequestPaymentMethodTypes { + payment_methods::RequestPaymentMethodTypes { + payment_method_type, + card_networks: Some(card_provider), + minimum_amount: Some(0), + maximum_amount: Some(68607706), + recurring_enabled: true, + installment_payment_enabled: false, + accepted_currencies: None, + accepted_countries: None, + payment_experience: None, + } + } + + pub fn get_payment_experience( + connector: Connector, + payment_method_type: PaymentMethodType, + payment_method: PaymentMethod, + ) -> Option { + match payment_method { + PaymentMethod::BankRedirect => None, + _ => match (connector, payment_method_type) { + #[cfg(feature = "dummy_connector")] + (Connector::DummyConnector4, _) | (Connector::DummyConnector7, _) => { + Some(api_models::enums::PaymentExperience::RedirectToUrl) + } + (Connector::Zen, GooglePay) | (Connector::Zen, ApplePay) => { + Some(api_models::enums::PaymentExperience::RedirectToUrl) + } + (Connector::Braintree, Paypal) | (Connector::Klarna, Klarna) => { + Some(api_models::enums::PaymentExperience::InvokeSdkClient) + } + (Connector::Globepay, AliPay) + | (Connector::Globepay, WeChatPay) + | (Connector::Stripe, WeChatPay) => { + Some(api_models::enums::PaymentExperience::DisplayQrCode) + } + (_, GooglePay) | (_, ApplePay) => { + Some(api_models::enums::PaymentExperience::InvokeSdkClient) + } + _ => Some(api_models::enums::PaymentExperience::RedirectToUrl), + }, + } + } + pub fn transform_payment_method( + connector: Connector, + provider: Vec, + payment_method: PaymentMethod, + ) -> Vec { + let mut payment_method_types = Vec::new(); + for method_type in provider { + let data = payment_methods::RequestPaymentMethodTypes { + payment_method_type: method_type, + card_networks: None, + minimum_amount: Some(0), + maximum_amount: Some(68607706), + recurring_enabled: true, + installment_payment_enabled: false, + accepted_currencies: None, + accepted_countries: None, + payment_experience: Self::get_payment_experience( + connector, + method_type, + payment_method, + ), + }; + payment_method_types.push(data) + } + payment_method_types + } + + pub fn create_connector_request( + request: Self, + api_response: ConnectorApiIntegrationPayload, + ) -> ConnectorApiIntegrationPayload { + let mut card_payment_method_types = Vec::new(); + let mut payment_method_enabled = Vec::new(); + + if let Some(payment_methods_enabled) = request.payment_methods_enabled.clone() { + for payload in payment_methods_enabled { + match payload.payment_method { + api_models::enums::PaymentMethod::Card => { + if let Some(card_provider) = payload.card_provider { + let payment_type = api_models::enums::PaymentMethodType::from_str( + &payload.payment_method_type, + ) + .map_err(|_| "Invalid key received".to_string()); + + if let Ok(payment_type) = payment_type { + for method in card_provider { + let data = payment_methods::RequestPaymentMethodTypes { + payment_method_type: payment_type, + card_networks: Some(vec![method]), + minimum_amount: Some(0), + maximum_amount: Some(68607706), + recurring_enabled: true, + installment_payment_enabled: false, + accepted_currencies: None, + accepted_countries: None, + payment_experience: None, + }; + card_payment_method_types.push(data) + } + } + } + } + + api_models::enums::PaymentMethod::Wallet + | api_models::enums::PaymentMethod::BankRedirect + | api_models::enums::PaymentMethod::PayLater + | api_models::enums::PaymentMethod::BankTransfer + | api_models::enums::PaymentMethod::Crypto + | api_models::enums::PaymentMethod::BankDebit + | api_models::enums::PaymentMethod::Reward + | api_models::enums::PaymentMethod::Upi + | api_models::enums::PaymentMethod::Voucher + | api_models::enums::PaymentMethod::GiftCard + | api_models::enums::PaymentMethod::CardRedirect => { + if let Some(provider) = payload.provider { + let val = Self::transform_payment_method( + request.connector, + provider, + payload.payment_method, + ); + if !val.is_empty() { + let methods = PaymentMethodsEnabled { + payment_method: payload.payment_method, + payment_method_types: Some(val), + }; + payment_method_enabled.push(methods); + } + } + } + }; + } + if !card_payment_method_types.is_empty() { + let card = PaymentMethodsEnabled { + payment_method: api_models::enums::PaymentMethod::Card, + payment_method_types: Some(card_payment_method_types), + }; + payment_method_enabled.push(card); + } + } + + let metadata = Self::transform_metedata(request); + ConnectorApiIntegrationPayload { + connector_type: api_response.connector_type, + profile_id: api_response.profile_id, + connector_name: api_response.connector_name, + connector_label: api_response.connector_label, + merchant_connector_id: api_response.merchant_connector_id, + disabled: api_response.disabled, + test_mode: api_response.test_mode, + payment_methods_enabled: Some(payment_method_enabled), + connector_webhook_details: api_response.connector_webhook_details, + metadata, + } + } + + pub fn transform_metedata(request: Self) -> Option { + let default_metadata = DashboardMetaData { + apple_pay_combined: None, + google_pay: None, + apple_pay: None, + account_name: None, + terminal_id: None, + merchant_account_id: None, + merchant_id: None, + merchant_config_currency: None, + }; + let meta_data = match request.metadata { + Some(data) => data, + None => default_metadata, + }; + let google_pay = Self::get_google_pay_details(meta_data.clone(), request.connector); + let account_name = meta_data.account_name.clone(); + let merchant_account_id = meta_data.merchant_account_id.clone(); + let merchant_id = meta_data.merchant_id.clone(); + let terminal_id = meta_data.terminal_id.clone(); + let apple_pay = meta_data.apple_pay; + let apple_pay_combined = meta_data.apple_pay_combined; + let merchant_config_currency = meta_data.merchant_config_currency; + Some(ApiModelMetaData { + google_pay, + apple_pay, + account_name, + merchant_account_id, + terminal_id, + merchant_id, + merchant_config_currency, + apple_pay_combined, + }) + } + + fn get_custom_gateway_name(connector: Connector) -> String { + match connector { + Connector::Checkout => String::from("checkoutltd"), + Connector::Nuvei => String::from("nuveidigital"), + Connector::Authorizedotnet => String::from("authorizenet"), + Connector::Globalpay => String::from("globalpayments"), + Connector::Bankofamerica | Connector::Cybersource => String::from("cybersource"), + _ => connector.to_string(), + } + } + fn get_google_pay_details( + meta_data: DashboardMetaData, + connector: Connector, + ) -> Option { + match meta_data.google_pay { + Some(gpay_data) => { + let google_pay_data = match gpay_data { + GooglePayData::Standard(data) => { + let token_parameter = payments::GpayTokenParameters { + gateway: Self::get_custom_gateway_name(connector), + gateway_merchant_id: data.gateway_merchant_id, + stripe_version: match connector { + Connector::Stripe => Some(String::from("2018-10-31")), + _ => None, + }, + stripe_publishable_key: match connector { + Connector::Stripe => data.stripe_publishable_key, + _ => None, + }, + }; + let merchant_info = payments::GpayMerchantInfo { + merchant_name: data.merchant_name, + merchant_id: data.merchant_id, + }; + let token_specification = payments::GpayTokenizationSpecification { + token_specification_type: String::from("PAYMENT_GATEWAY"), + parameters: token_parameter, + }; + let allowed_payment_methods_parameters = + payments::GpayAllowedMethodsParameters { + allowed_auth_methods: vec![ + "PAN_ONLY".to_string(), + "CRYPTOGRAM_3DS".to_string(), + ], + allowed_card_networks: vec![ + "AMEX".to_string(), + "DISCOVER".to_string(), + "INTERAC".to_string(), + "JCB".to_string(), + "MASTERCARD".to_string(), + "VISA".to_string(), + ], + }; + let allowed_payment_methods = payments::GpayAllowedPaymentMethods { + payment_method_type: String::from("CARD"), + parameters: allowed_payment_methods_parameters, + tokenization_specification: token_specification, + }; + GoogleApiModelData::Standard(payments::GpayMetaData { + merchant_info, + allowed_payment_methods: vec![allowed_payment_methods], + }) + } + GooglePayData::Zen(data) => GoogleApiModelData::Zen(data), + }; + Some(google_pay_data) + } + _ => None, + } + } +} diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml new file mode 100644 index 000000000000..b69b1aabaff2 --- /dev/null +++ b/crates/connector_configs/toml/development.toml @@ -0,0 +1,789 @@ + +[aci] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["ali_pay","mb_way"] +bank_redirect=["ideal","giropay","sofort","eps","przelewy24","trustly","interac"] +[aci.connector_auth.BodyKey] +api_key="API Key" +key1="Entity ID" +[aci.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","affirm","afterpay_clearpay","pay_bright","walley", "alma", "atome"] +bank_debit=["ach","bacs","sepa"] +bank_redirect=["ideal","giropay","sofort","eps","blik","przelewy24","trustly","online_banking_czech_republic","online_banking_finland","online_banking_poland","online_banking_slovakia","bancontact_card", "online_banking_fpx", "online_banking_thailand", "bizum", "open_banking_uk"] +bank_transfer = ["permata_bank_transfer", "bca_bank_transfer", "bni_va", "bri_va", "cimb_va", "danamon_va", "mandiri_va"] +wallet = ["apple_pay","google_pay","paypal","we_chat_pay","ali_pay","mb_way", "ali_pay_hk", "go_pay", "kakao_pay", "twint", "gcash", "vipps", "dana", "momo", "swish", "touch_n_go"] +voucher = ["boleto", "alfamart", "indomaret", "oxxo", "seven_eleven", "lawson", "mini_stop", "family_mart", "seicomart", "pay_easy"] +gift_card = ["pay_safe_card", "givex"] +card_redirect = ["benefit", "knet", "momo_atm"] +[adyen.connector_auth.BodyKey] +api_key="Adyen API Key" +key1="Adyen Account Id" +[adyen.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[adyen.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[adyen.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[airwallex] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay"] +body_type="BodyKey" +[airwallex.connector_auth.BodyKey] +api_key="API Key" +key1="Client ID" +[airwallex.connector_webhook_details] +merchant_secret="Source verification key" + +[authorizedotnet] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","paypal"] +body_type="BodyKey" +[authorizedotnet.connector_auth.BodyKey] +api_key="API Login ID" +key1="Transaction Key" +[authorizedotnet.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[authorizedotnet.connector_webhook_details] +merchant_secret="Source verification key" + +[bitpay] +crypto = ["crypto_currency"] +[bitpay.connector_auth.HeaderKey] +api_key="API Key" +[bitpay.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","apple_pay"] +[bluesnap.connector_auth.BodyKey] +api_key="Password" +key1="Username" +[bluesnap.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bluesnap.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bluesnap.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[bluesnap.metadata] +merchant_id="Merchant Id" + + +[braintree] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[braintree.connector_webhook_details] +merchant_secret="Source verification key" + + +[braintree.connector_auth.SignatureKey] +api_key="Public Key" +key1="Merchant Id" +api_secret="Private Key" +[braintree.metadata] +merchant_account_id="Merchant Account Id" +merchant_config_currency="Currency" + +[checkout] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay","paypal"] +[checkout.connector_auth.SignatureKey] +api_key="Checkout API Public Key" +key1="Processing Channel ID" +api_secret="Checkout API Secret Key" +[checkout.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[checkout.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[checkout.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[coinbase] +crypto = ["crypto_currency"] +[coinbase.connector_auth.HeaderKey] +api_key="API Key" +[coinbase.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +[cybersource.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[cybersource.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[cybersource.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cybersource.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[iatapay] +upi=["upi_collect"] +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + +[opennode] +crypto = ["crypto_currency"] +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[prophetpay] +card_redirect = ["card_redirect"] +[prophetpay.connector_auth.SignatureKey] +api_key="Username" +key1="Token" +api_secret="Profile" + + +[bambora] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","paypal"] +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[boku] +wallet = ["dana","gcash","go_pay","kakao_pay","momo"] +[boku.connector_auth.BodyKey] +api_key="API KEY" +key1= "MERCHANT ID" +[boku.connector_webhook_details] +merchant_secret="Source verification key" + +[dlocal] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[dlocal.connector_auth.SignatureKey] +api_key="X Login" +key1="X Trans Key" +api_secret="Secret Key" +[dlocal.connector_webhook_details] +merchant_secret="Source verification key" + +[fiserv] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[fiserv.connector_auth.SignatureKey] +api_key="API Key" +key1="Merchant ID" +api_secret="API Secret" +[fiserv.metadata] +terminal_id="Terminal ID" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" + +[forte] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[forte.connector_auth.MultiAuthKey] +api_key="API Access ID" +key1="Organization ID" +api_secret="API Secure Key" +key2="Location ID" +[forte.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["google_pay","paypal"] +[globalpay.connector_auth.BodyKey] +api_key="Global App Key" +key1="Global App ID" +[globalpay.metadata] +account_name="Account Name" +[globalpay.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[klarna] +pay_later=["klarna"] +[klarna.connector_auth.HeaderKey] +api_key="Klarna API Key" +[klarna.connector_webhook_details] +merchant_secret="Source verification key" + +[mollie] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps","przelewy24","bancontact_card"] +wallet = ["paypal"] +[mollie.connector_auth.BodyKey] +api_key="API Key" +key1="Profile Token" +[mollie.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","paypal"] +[multisafepay.connector_auth.HeaderKey] +api_key="Enter API Key" +[multisafepay.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nexinets] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["apple_pay","paypal"] +[nexinets.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant ID" +[nexinets.connector_webhook_details] +merchant_secret="Source verification key" + +[nexinets.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nexinets.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nmi] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +[nmi.connector_auth.HeaderKey] +api_key="API Key" +[nmi.connector_webhook_details] +merchant_secret="Source verification key" + +[nmi.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nmi.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nmi.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[noon] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay","paypal"] +[noon.connector_auth.SignatureKey] +api_key="API Key" +key1="Business Identifier" +api_secret="Application Identifier" +[noon.connector_webhook_details] +merchant_secret="Source verification key" + +[noon.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[noon.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[noon.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nuvei] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","afterpay_clearpay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["apple_pay","google_pay","paypal"] +[nuvei.connector_auth.SignatureKey] +api_key="Merchant ID" +key1="Merchant Site ID" +api_secret="Merchant Secret" +[nuvei.connector_webhook_details] +merchant_secret="Source verification key" + +[nuvei.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nuvei.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nuvei.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[paypal] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["paypal"] +bank_redirect=["ideal","giropay","sofort","eps"] +is_verifiable = true +[paypal.connector_auth.BodyKey] +api_key="Client Secret" +key1="Client ID" +[paypal.connector_webhook_details] +merchant_secret="Source verification key" + +[payu] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay"] +[payu.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant POS ID" +[payu.connector_webhook_details] +merchant_secret="Source verification key" + +[payu.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[rapyd] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay"] +[rapyd.connector_auth.BodyKey] +api_key="Access Key" +key1="API Secret" +[rapyd.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[rapyd.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[shift4] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +[shift4.connector_auth.HeaderKey] +api_key="API Key" +[shift4.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","affirm","afterpay_clearpay"] +bank_redirect=["ideal","giropay","sofort","eps","bancontact_card","przelewy24"] +bank_debit=["ach","bacs","becs","sepa"] +bank_transfer=["ach","bacs","sepa", "multibanco"] +wallet = ["apple_pay","google_pay","we_chat_pay","ali_pay", "cashapp"] +is_verifiable = true +[stripe.connector_auth.HeaderKey] +api_key="Secret Key" +[stripe.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +stripe_publishable_key="Stripe Publishable Key" +merchant_id="Google Pay Merchant ID" + +[stripe.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[stripe.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[zen] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +voucher = ["boleto", "efecty", "pago_efectivo", "red_compra", "red_pagos"] +bank_transfer = ["pix", "pse"] +wallet = ["apple_pay","google_pay"] +[zen.connector_auth.HeaderKey] +api_key="API Key" +[zen.connector_webhook_details] +merchant_secret="Source verification key" + +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + +[trustpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps","blik"] +wallet = ["apple_pay","google_pay"] +[trustpay.connector_auth.SignatureKey] +api_key="API Key" +key1="Project ID" +api_secret="Secret Key" +[trustpay.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[trustpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[worldline] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay"] +[worldline.connector_auth.SignatureKey] +api_key="API Key ID" +key1="Merchant ID" +api_secret="Secret API Key" +[worldline.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","apple_pay"] +[worldpay.connector_auth.BodyKey] +api_key="Username" +key1="Password" +[worldpay.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[worldpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[worldpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cashtocode] +reward = ["classic", "evoucher"] +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + +[cryptopay] +crypto = ["crypto_currency"] +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + +[dummy_connector] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[dummy_connector.connector_auth.HeaderKey] +api_key="Api Key" + +[helcim] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[helcim.connector_auth.HeaderKey] +api_key="Api Key" +[helcim.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe_test] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","ali_pay","we_chat_pay"] +pay_later=["klarna","affirm","afterpay_clearpay"] +[stripe_test.connector_auth.HeaderKey] +api_key="Api Key" + + +[paypal_test] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["paypal"] +[paypal_test.connector_auth.HeaderKey] +api_key="Api Key" + +[payme] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + +[powertranz] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + +[globepay] +wallet = ["we_chat_pay","ali_pay"] +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[tsys] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[square] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[square_payout.connector_auth.BodyKey] +api_key = "Square API Key" +key1 = "Square Client Id" +[square.connector_webhook_details] +merchant_secret="Source verification key" + +[stax] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_debit=["ach"] +[stax.connector_auth.HeaderKey] +api_key="Api Key" +[stax.connector_webhook_details] +merchant_secret="Source verification key" + +[volt] +bank_redirect = ["open_banking_uk"] +[volt.connector_auth.MultiAuthKey] +api_key = "Username" +api_secret = "Password" +key1 = "Client ID" +key2 = "Client Secret" + +[wise_payout] +bank_transfer = ["ach","bacs","sepa"] +[wise_payout.connector_auth.BodyKey] +api_key = "Wise API Key" +key1 = "Wise Account Id" + +[adyen_payout] +bank_transfer = ["ach","bacs","sepa"] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[adyen_payout.connector_auth.SignatureKey] +api_key = "Adyen API Key (Payout creation)" +api_secret = "Adyen Key (Payout submission)" +key1 = "Adyen Account Id" + +[gocardless] +bank_debit=["ach","becs","sepa"] +[gocardless.connector_auth.HeaderKey] +api_key="Access Token" +[gocardless.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] + +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[placetopay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] + +[placetopay.connector_auth.BodyKey] +api_key="Login" +key1="Trankey" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml new file mode 100644 index 000000000000..225fc63912fc --- /dev/null +++ b/crates/connector_configs/toml/production.toml @@ -0,0 +1,614 @@ + +[aci] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["ali_pay","mb_way"] +bank_redirect=["ideal","giropay","sofort","eps","przelewy24","trustly"] +[aci.connector_auth.BodyKey] +api_key="API Key" +key1="Entity ID" +[aci.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","affirm","afterpay_clearpay"] +bank_debit=["ach","bacs"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["apple_pay","google_pay","paypal"] +[adyen.connector_auth.BodyKey] +api_key="Adyen API Key" +key1="Adyen Account Id" +[adyen.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[adyen.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[adyen.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[airwallex] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +body_type="BodyKey" +[airwallex.connector_auth.BodyKey] +api_key="API Key" +key1="Client ID" +[airwallex.connector_webhook_details] +merchant_secret="Source verification key" + +[authorizedotnet] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","paypal"] +body_type="BodyKey" +[authorizedotnet.connector_auth.BodyKey] +api_key="API Login ID" +key1="Transaction Key" +[authorizedotnet.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[authorizedotnet.connector_webhook_details] +merchant_secret="Source verification key" + +[bitpay] +crypto = ["crypto_currency"] +[bitpay.connector_auth.HeaderKey] +api_key="API Key" +[bitpay.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","apple_pay"] +[bluesnap.connector_auth.BodyKey] +api_key="Password" +key1="Username" +[bluesnap.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bluesnap.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bluesnap.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[bluesnap.metadata] +merchant_id="Merchant Id" + +[braintree] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] + +[braintree.connector_auth.SignatureKey] +api_key="Public Key" +key1="Merchant Id" +api_secret="Private Key" +[braintree.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +[checkout.connector_auth.SignatureKey] +api_key="Checkout API Public Key" +key1="Processing Channel ID" +api_secret="Checkout API Secret Key" +[checkout.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[checkout.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[checkout.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[coinbase] +crypto = ["crypto_currency"] +[coinbase.connector_auth.HeaderKey] +api_key="API Key" +[coinbase.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +[cybersource.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[cybersource.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[cybersource.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cybersource.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[iatapay] +upi=["upi_collect"] +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + +[opennode] +crypto = ["crypto_currency"] +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","paypal"] +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[dlocal] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[dlocal.connector_auth.SignatureKey] +api_key="X Login" +key1="X Trans Key" +api_secret="Secret Key" +[dlocal.connector_webhook_details] +merchant_secret="Source verification key" + + +[fiserv] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[fiserv.connector_auth.SignatureKey] +api_key="API Key" +key1="Merchant ID" +api_secret="API Secret" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" +[fiserv.metadata] +terminal_id="Terminal ID" + +[forte] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[forte.connector_auth.MultiAuthKey] +api_key="API Access ID" +key1="Organization ID" +api_secret="API Secure Key" +key2="Location ID" +[forte.connector_webhook_details] +merchant_secret="Source verification key" + + +[globalpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["google_pay","paypal"] +[globalpay.connector_auth.BodyKey] +api_key="Global App Key" +key1="Global App ID" +[globalpay.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay.metadata] +account_name="Account Name" + +[globalpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[klarna] +pay_later=["klarna"] +[klarna.connector_auth.HeaderKey] +api_key="Klarna API Key" +[klarna.connector_webhook_details] +merchant_secret="Source verification key" + +[mollie] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["paypal"] +[mollie.connector_auth.BodyKey] +api_key="API Key" +key1="Profile Token" +[mollie.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[multisafepay.connector_auth.HeaderKey] +api_key="Enter API Key" +[multisafepay.connector_webhook_details] +merchant_secret="Source verification key" + + +[nexinets] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["apple_pay","paypal"] +[nexinets.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant ID" +[nexinets.connector_webhook_details] +merchant_secret="Source verification key" + +[nexinets.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nexinets.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nmi] +bank_redirect=["ideal"] +[nmi.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[nmi.connector_webhook_details] +merchant_secret="Source verification key" + +[nuvei] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[nuvei.connector_auth.SignatureKey] +api_key="Merchant ID" +key1="Merchant Site ID" +api_secret="Merchant Secret" +[nuvei.connector_webhook_details] +merchant_secret="Source verification key" + +[paypal] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["paypal"] +is_verifiable = true +[paypal.connector_auth.BodyKey] +api_key="Client Secret" +key1="Client ID" +[paypal.connector_webhook_details] +merchant_secret="Source verification key" + +[payu] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay"] +[payu.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant POS ID" +[payu.connector_webhook_details] +merchant_secret="Source verification key" + +[payu.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[rapyd] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay"] +[rapyd.connector_auth.BodyKey] +api_key="Access Key" +key1="API Secret" +[rapyd.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[rapyd.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[shift4] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +[shift4.connector_auth.HeaderKey] +api_key="API Key" +[shift4.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","affirm","afterpay_clearpay"] +bank_redirect=["ideal","giropay","sofort","eps"] +bank_debit=["ach","becs","sepa"] +bank_transfer=["ach","bacs","sepa"] +wallet = ["apple_pay","google_pay"] +is_verifiable = true +[stripe.connector_auth.HeaderKey] +api_key="Secret Key" +[stripe.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +stripe_publishable_key="Stripe Publishable Key" +merchant_id="Google Pay Merchant ID" + +[stripe.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[stripe.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[zen] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +voucher = ["boleto", "efecty", "pago_efectivo", "red_compra", "red_pagos"] +bank_transfer = ["pix", "pse"] +[zen.connector_auth.HeaderKey] +api_key="API Key" +[zen.connector_webhook_details] +merchant_secret="Source verification key" + +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + + +[trustpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps", "blik"] +wallet = ["apple_pay","google_pay"] +[trustpay.connector_auth.SignatureKey] +api_key="API Key" +key1="Project ID" +api_secret="Secret Key" +[trustpay.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[trustpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[worldline] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay"] +[worldline.connector_auth.SignatureKey] +api_key="API Key ID" +key1="Merchant ID" +api_secret="Secret API Key" +[worldline.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","apple_pay"] +[worldpay.connector_auth.BodyKey] +api_key="Username" +key1="Password" + +[worldpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[worldpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[worldpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[worldpay.connector_webhook_details] +merchant_secret="Source verification key" + +[dummy_connector] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] + +[payme] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + +[powertranz] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + +[globepay] +wallet = ["we_chat_pay","ali_pay"] +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[tsys] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[cashtocode] +reward = ["classic", "evoucher"] +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + +[cryptopay] +crypto = ["crypto_currency"] +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] + +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml new file mode 100644 index 000000000000..44a8806f0fee --- /dev/null +++ b/crates/connector_configs/toml/sandbox.toml @@ -0,0 +1,786 @@ + +[aci] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["ali_pay","mb_way"] +bank_redirect=["ideal","giropay","sofort","eps","przelewy24","trustly","interac"] +[aci.connector_auth.BodyKey] +api_key="API Key" +key1="Entity ID" +[aci.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","affirm","afterpay_clearpay","pay_bright","walley", "alma", "atome"] +bank_debit=["ach","bacs","sepa"] +bank_redirect=["ideal","giropay","sofort","eps","blik","przelewy24","trustly","online_banking_czech_republic","online_banking_finland","online_banking_poland","online_banking_slovakia","bancontact_card", "online_banking_fpx", "online_banking_thailand", "bizum", "open_banking_uk"] +wallet = ["apple_pay","google_pay","paypal","we_chat_pay","ali_pay","mb_way", "ali_pay_hk", "go_pay", "kakao_pay", "twint", "gcash", "vipps", "momo", "dana", "swish", "touch_n_go"] +bank_transfer = ["permata_bank_transfer", "bca_bank_transfer", "bni_va", "bri_va", "cimb_va", "danamon_va", "mandiri_va"] +voucher = ["boleto", "alfamart", "indomaret", "oxxo", "seven_eleven", "lawson", "mini_stop", "family_mart", "seicomart", "pay_easy"] +gift_card = ["pay_safe_card", "givex"] +card_redirect = ["benefit", "knet", "momo_atm"] + +[adyen.connector_auth.BodyKey] +api_key="Adyen API Key" +key1="Adyen Account Id" +[adyen.connector_webhook_details] +merchant_secret="Source verification key" + +[adyen.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[adyen.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[adyen.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[airwallex] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay"] +body_type="BodyKey" +[airwallex.connector_auth.BodyKey] +api_key="API Key" +key1="Client ID" +[airwallex.connector_webhook_details] +merchant_secret="Source verification key" + +[authorizedotnet] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","paypal"] +body_type="BodyKey" +[authorizedotnet.connector_auth.BodyKey] +api_key="API Login ID" +key1="Transaction Key" +[authorizedotnet.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" +[authorizedotnet.connector_webhook_details] +merchant_secret="Source verification key" + +[bitpay] +crypto = ["crypto_currency"] +[bitpay.connector_auth.HeaderKey] +api_key="API Key" +[bitpay.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","apple_pay"] +[bluesnap.connector_auth.BodyKey] +api_key="Password" +key1="Username" +[bluesnap.connector_webhook_details] +merchant_secret="Source verification key" + +[bluesnap.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[bluesnap.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bluesnap.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" +[bluesnap.metadata] +merchant_id="Merchant Id" + + +[braintree] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] + +[braintree.connector_auth.SignatureKey] +api_key="Public Key" +key1="Merchant Id" +api_secret="Private Key" +[braintree.connector_webhook_details] +merchant_secret="Source verification key" +[braintree.metadata] +merchant_account_id="Merchant Account Id" +merchant_config_currency="Currency" + +[checkout] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay","paypal"] +[checkout.connector_auth.SignatureKey] +api_key="Checkout API Public Key" +key1="Processing Channel ID" +api_secret="Checkout API Secret Key" +[checkout.connector_webhook_details] +merchant_secret="Source verification key" + +[checkout.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[checkout.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[checkout.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + + + +[coinbase] +crypto = ["crypto_currency"] +[coinbase.connector_auth.HeaderKey] +api_key="API Key" +[coinbase.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +[cybersource.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[cybersource.connector_webhook_details] +merchant_secret="Source verification key" + +[cybersource.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[cybersource.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cybersource.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[helcim] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[helcim.connector_auth.HeaderKey] +api_key="Api Key" +[helcim.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +upi=["upi_collect"] +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + +[opennode] +crypto = ["crypto_currency"] +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","paypal"] +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[boku] +wallet = ["dana","gcash","go_pay","kakao_pay","momo"] +[boku.connector_auth.BodyKey] +api_key="API KEY" +key1= "MERCHANT ID" +[boku.connector_webhook_details] +merchant_secret="Source verification key" + +[dlocal] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[dlocal.connector_auth.SignatureKey] +api_key="X Login" +key1="X Trans Key" +api_secret="Secret Key" +[dlocal.connector_webhook_details] +merchant_secret="Source verification key" + +[fiserv] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[fiserv.connector_auth.SignatureKey] +api_key="API Key" +key1="Merchant ID" +api_secret="API Secret" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" +[fiserv.metadata] +terminal_id="Terminal ID" + +[forte] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[forte.connector_auth.MultiAuthKey] +api_key="API Access ID" +key1="Organization ID" +api_secret="API Secure Key" +key2="Location ID" +[forte.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["google_pay","paypal"] +[globalpay.connector_auth.BodyKey] +api_key="Global App Key" +key1="Global App ID" +[globalpay.metadata] +account_name="Account Name" +[globalpay.connector_webhook_details] +merchant_secret="Source verification key" + +[globalpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[klarna] +pay_later=["klarna"] +[klarna.connector_auth.HeaderKey] +api_key="Klarna API Key" +[klarna.connector_webhook_details] +merchant_secret="Source verification key" + +[mollie] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps","przelewy24","bancontact_card"] +wallet = ["paypal"] +[mollie.connector_auth.BodyKey] +api_key="API Key" +key1="Profile Token" +[mollie.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","paypal"] +[multisafepay.connector_auth.HeaderKey] +api_key="Enter API Key" +[multisafepay.connector_webhook_details] +merchant_secret="Source verification key" + +[multisafepay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nexinets] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["apple_pay","paypal"] +[nexinets.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant ID" +[nexinets.connector_webhook_details] +merchant_secret="Source verification key" + +[nexinets.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nexinets.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nmi] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] +[nmi.connector_auth.HeaderKey] +api_key="API Key" +[nmi.connector_webhook_details] +merchant_secret="Source verification key" + +[nmi.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nmi.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nmi.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[noon] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay","paypal"] +[noon.connector_auth.SignatureKey] +api_key="API Key" +key1="Business Identifier" +api_secret="Application Identifier" +[noon.connector_webhook_details] +merchant_secret="Source verification key" + +[noon.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[noon.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[noon.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[nuvei] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","afterpay_clearpay"] +bank_redirect=["ideal","giropay","sofort","eps"] +wallet = ["apple_pay","google_pay","paypal"] +[nuvei.connector_auth.SignatureKey] +api_key="Merchant ID" +key1="Merchant Site ID" +api_secret="Merchant Secret" +[nuvei.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[nuvei.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[nuvei.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[paypal] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["paypal"] +bank_redirect=["ideal","giropay","sofort","eps"] +is_verifiable = true +[paypal.connector_auth.BodyKey] +api_key="Client Secret" +key1="Client ID" +[paypal.connector_webhook_details] +merchant_secret="Source verification key" + +[payu] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay"] +[payu.connector_auth.BodyKey] +api_key="API Key" +key1="Merchant POS ID" +[payu.connector_webhook_details] +merchant_secret="Source verification key" + +[payu.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[prophetpay] +card_redirect = ["card_redirect"] +[prophetpay.connector_auth.SignatureKey] +api_key="Username" +key1="Token" +api_secret="Profile" + + +[rapyd] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay"] +[rapyd.connector_auth.BodyKey] +api_key="Access Key" +key1="API Secret" +[rapyd.connector_webhook_details] +merchant_secret="Source verification key" + +[rapyd.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[rapyd.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[shift4] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps"] +[shift4.connector_auth.HeaderKey] +api_key="API Key" +[shift4.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +pay_later=["klarna","affirm","afterpay_clearpay"] +bank_redirect=["ideal","giropay","sofort","eps","bancontact_card","przelewy24"] +bank_debit=["ach","bacs","becs","sepa"] +bank_transfer=["ach","bacs","sepa", "multibanco"] +wallet = ["apple_pay","google_pay","we_chat_pay","ali_pay", "cashapp"] +is_verifiable = true +[stripe.connector_auth.HeaderKey] +api_key="Secret Key" +[stripe.connector_webhook_details] +merchant_secret="Source verification key" + +[stripe.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +stripe_publishable_key="Stripe Publishable Key" +merchant_id="Google Pay Merchant ID" + +[stripe.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[stripe.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[zen] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +voucher = ["boleto", "efecty", "pago_efectivo", "red_compra", "red_pagos"] +bank_transfer = ["pix", "pse"] +wallet = ["apple_pay","google_pay"] +[zen.connector_auth.HeaderKey] +api_key="API Key" +[zen.connector_webhook_details] +merchant_secret="Source verification key" + +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + + +[trustpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay","sofort","eps","blik"] +wallet = ["apple_pay","google_pay"] +[trustpay.connector_auth.SignatureKey] +api_key="API Key" +key1="Project ID" +api_secret="Secret Key" +[trustpay.connector_webhook_details] +merchant_secret="Source verification key" + +[trustpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[trustpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[worldline] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_redirect=["ideal","giropay"] +[worldline.connector_auth.SignatureKey] +api_key="API Key ID" +key1="Merchant ID" +api_secret="Secret API Key" +[worldline.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","apple_pay"] +[worldpay.connector_auth.BodyKey] +api_key="Username" +key1="Password" +[worldpay.connector_webhook_details] +merchant_secret="Source verification key" + +[worldpay.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[worldpay.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[worldpay.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[cashtocode] +reward = ["classic", "evoucher"] +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + +[cryptopay] +crypto = ["crypto_currency"] +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + +[dummy_connector] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[dummy_connector.connector_auth.HeaderKey] +api_key="Api Key" + +[stripe_test] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["google_pay","ali_pay"] +pay_later=["klarna","affirm","afterpay_clearpay"] +[stripe_test.connector_auth.HeaderKey] +api_key="Api Key" + +[paypal_test] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["paypal"] +[paypal_test.connector_auth.HeaderKey] +api_key="Api Key" + +[payme] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + +[powertranz] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + +[globepay] +wallet = ["we_chat_pay","ali_pay"] +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[tsys] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[square] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[square_payout.connector_auth.BodyKey] +api_key = "Square API Key" +key1 = "Square Client Id" +[square.connector_webhook_details] +merchant_secret="Source verification key" + +[stax] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +bank_debit=["ach"] +[stax.connector_auth.HeaderKey] +api_key="Api Key" +[stax.connector_webhook_details] +merchant_secret="Source verification key" + +[volt] +bank_redirect = ["open_banking_uk"] +[volt.connector_auth.MultiAuthKey] +api_key = "Username" +api_secret = "Password" +key1 = "Client ID" +key2 = "Client Secret" + +[wise_payout] +bank_transfer = ["ach","bacs","sepa"] +[wise_payout.connector_auth.BodyKey] +api_key = "Wise API Key" +key1 = "Wise Account Id" + +[adyen_payout] +bank_transfer = ["ach","bacs","sepa"] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[adyen_payout.connector_auth.SignatureKey] +api_key = "Adyen API Key (Payout creation)" +api_secret = "Adyen Key (Payout submission)" +key1 = "Adyen Account Id" + +[gocardless] +bank_debit=["ach","becs","sepa"] +[gocardless.connector_auth.HeaderKey] +api_key="Access Token" +[gocardless.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +wallet = ["apple_pay","google_pay"] + +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[placetopay] +credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] + +[placetopay.connector_auth.BodyKey] +api_key="Login" +key1="Trankey" \ No newline at end of file diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index f17cdec8759a..d9f5330a1b8d 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -14,11 +14,15 @@ default = ["connector_choice_bcompat", "connector_choice_mca_id"] release = ["connector_choice_bcompat", "connector_choice_mca_id"] connector_choice_bcompat = ["api_models/connector_choice_bcompat"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] -dummy_connector = ["kgraph_utils/dummy_connector"] +dummy_connector = ["kgraph_utils/dummy_connector", "connector_configs/dummy_connector"] +production = ["connector_configs/production"] +development = ["connector_configs/development"] +sandbox = ["connector_configs/sandbox"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } currency_conversion = { version = "0.1.0", path = "../currency_conversion" } +connector_configs = { version = "0.1.0", path = "../connector_configs" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index 134016191f53..1143ea8a2817 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,10 +7,14 @@ use std::{ }; use api_models::{ - admin as admin_api, conditional_configs::ConditionalConfigs, routing::ConnectorSelection, - surcharge_decision_configs::SurchargeDecisionConfigs, + admin as admin_api, conditional_configs::ConditionalConfigs, enums as api_model_enums, + routing::ConnectorSelection, surcharge_decision_configs::SurchargeDecisionConfigs, }; use common_enums::RoutableConnectors; +use connector_configs::{ + common_config::{ConnectorApiIntegrationPayload, DashboardRequestPayload}, + connector, +}; use currency_conversion::{ conversion::convert as convert_currency, types as currency_conversion_types, }; @@ -291,3 +295,35 @@ pub fn get_description_category() -> JsResult { Ok(serde_wasm_bindgen::to_value(&category)?) } + +#[wasm_bindgen(js_name = getConnectorConfig)] +pub fn get_connector_config(key: &str) -> JsResult { + let key = api_model_enums::Connector::from_str(key) + .map_err(|_| "Invalid key received".to_string())?; + let res = connector::ConnectorConfig::get_connector_config(key)?; + Ok(serde_wasm_bindgen::to_value(&res)?) +} + +#[cfg(feature = "payouts")] +#[wasm_bindgen(js_name = getPayoutConnectorConfig)] +pub fn get_payout_connector_config(key: &str) -> JsResult { + let key = api_model_enums::PayoutConnectors::from_str(key) + .map_err(|_| "Invalid key received".to_string())?; + let res = connector::ConnectorConfig::get_payout_connector_config(key)?; + Ok(serde_wasm_bindgen::to_value(&res)?) +} + +#[wasm_bindgen(js_name = getRequestPayload)] +pub fn get_request_payload(input: JsValue, response: JsValue) -> JsResult { + let input: DashboardRequestPayload = serde_wasm_bindgen::from_value(input)?; + let api_response: ConnectorApiIntegrationPayload = serde_wasm_bindgen::from_value(response)?; + let result = DashboardRequestPayload::create_connector_request(input, api_response); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +#[wasm_bindgen(js_name = getResponsePayload)] +pub fn get_response_payload(input: JsValue) -> JsResult { + let input: ConnectorApiIntegrationPayload = serde_wasm_bindgen::from_value(input)?; + let result = ConnectorApiIntegrationPayload::get_transformed_response_payload(input); + Ok(serde_wasm_bindgen::to_value(&result)?) +} diff --git a/docker/wasm-build.Dockerfile b/docker/wasm-build.Dockerfile new file mode 100644 index 000000000000..2ebdcec217ef --- /dev/null +++ b/docker/wasm-build.Dockerfile @@ -0,0 +1,25 @@ +FROM rust:latest as builder + +ARG RUN_ENV=sandbox +ARG EXTRA_FEATURES="" + +RUN apt-get update \ + && apt-get install -y libssl-dev pkg-config + +ENV CARGO_INCREMENTAL=0 +# Allow more retries for network requests in cargo (downloading crates) and +# rustup (installing toolchains). This should help to reduce flaky CI failures +# from transient network timeouts or other issues. +ENV CARGO_NET_RETRY=10 +ENV RUSTUP_MAX_RETRIES=10 +# Don't emit giant backtraces in the CI logs. +ENV RUST_BACKTRACE="short" +ENV env=$env +COPY . . +RUN echo env +RUN cargo install wasm-pack +RUN wasm-pack build --target web --out-dir /tmp/wasm --out-name euclid crates/euclid_wasm -- --features ${RUN_ENV},${EXTRA_FEATURES} + +FROM scratch + +COPY --from=builder /tmp/wasm /tmp From 396a64f3bbad6e75d4b263286a7ef6a2f09b180e Mon Sep 17 00:00:00 2001 From: Kashif Soofi Date: Tue, 19 Dec 2023 18:04:57 +0000 Subject: [PATCH 233/443] feat(db): Implement `AuthorizationInterface` for `MockDb` (#3151) --- crates/diesel_models/src/authorization.rs | 15 +++++ crates/router/src/db/authorization.rs | 68 +++++++++++++++++++---- crates/storage_impl/src/mock_db.rs | 2 + 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/crates/diesel_models/src/authorization.rs b/crates/diesel_models/src/authorization.rs index 64fd1c65187d..b6f75bbb9b77 100644 --- a/crates/diesel_models/src/authorization.rs +++ b/crates/diesel_models/src/authorization.rs @@ -57,6 +57,21 @@ pub struct AuthorizationUpdateInternal { pub connector_authorization_id: Option, } +impl AuthorizationUpdateInternal { + pub fn create_authorization(self, source: Authorization) -> Authorization { + Authorization { + status: self.status.unwrap_or(source.status), + error_code: self.error_code.or(source.error_code), + error_message: self.error_message.or(source.error_message), + modified_at: self.modified_at.unwrap_or(common_utils::date_time::now()), + connector_authorization_id: self + .connector_authorization_id + .or(source.connector_authorization_id), + ..source + } + } +} + impl From for AuthorizationUpdateInternal { fn from(authorization_child_update: AuthorizationUpdate) -> Self { let now = Some(common_utils::date_time::now()); diff --git a/crates/router/src/db/authorization.rs b/crates/router/src/db/authorization.rs index f24daaf718ad..d167d1775375 100644 --- a/crates/router/src/db/authorization.rs +++ b/crates/router/src/db/authorization.rs @@ -1,3 +1,4 @@ +use diesel_models::authorization::AuthorizationUpdateInternal; use error_stack::IntoReport; use super::{MockDb, Store}; @@ -77,28 +78,71 @@ impl AuthorizationInterface for Store { impl AuthorizationInterface for MockDb { async fn insert_authorization( &self, - _authorization: storage::AuthorizationNew, + authorization: storage::AuthorizationNew, ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? + let mut authorizations = self.authorizations.lock().await; + if authorizations.iter().any(|authorization_inner| { + authorization_inner.authorization_id == authorization.authorization_id + }) { + Err(errors::StorageError::DuplicateValue { + entity: "authorization_id", + key: None, + })? + } + let authorization = storage::Authorization { + authorization_id: authorization.authorization_id, + merchant_id: authorization.merchant_id, + payment_id: authorization.payment_id, + amount: authorization.amount, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + status: authorization.status, + error_code: authorization.error_code, + error_message: authorization.error_message, + connector_authorization_id: authorization.connector_authorization_id, + previously_authorized_amount: authorization.previously_authorized_amount, + }; + authorizations.push(authorization.clone()); + Ok(authorization) } async fn find_all_authorizations_by_merchant_id_payment_id( &self, - _merchant_id: &str, - _payment_id: &str, + merchant_id: &str, + payment_id: &str, ) -> CustomResult, errors::StorageError> { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? + let authorizations = self.authorizations.lock().await; + let authorizations_found: Vec = authorizations + .iter() + .filter(|a| a.merchant_id == merchant_id && a.payment_id == payment_id) + .cloned() + .collect(); + + Ok(authorizations_found) } async fn update_authorization_by_merchant_id_authorization_id( &self, - _merchant_id: String, - _authorization_id: String, - _authorization: storage::AuthorizationUpdate, + merchant_id: String, + authorization_id: String, + authorization_update: storage::AuthorizationUpdate, ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? + let mut authorizations = self.authorizations.lock().await; + authorizations + .iter_mut() + .find(|authorization| authorization.authorization_id == authorization_id && authorization.merchant_id == merchant_id) + .map(|authorization| { + let authorization_updated = + AuthorizationUpdateInternal::from(authorization_update) + .create_authorization(authorization.clone()); + *authorization = authorization_updated.clone(); + authorization_updated + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "cannot find authorization for authorization_id = {authorization_id} and merchant_id = {merchant_id}" + )) + .into(), + ) } } diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index e22d39ce70c8..a6ba763bd916 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -43,6 +43,7 @@ pub struct MockDb { pub organizations: Arc>>, pub users: Arc>>, pub user_roles: Arc>>, + pub authorizations: Arc>>, pub dashboard_metadata: Arc>>, } @@ -79,6 +80,7 @@ impl MockDb { organizations: Default::default(), users: Default::default(), user_roles: Default::default(), + authorizations: Default::default(), dashboard_metadata: Default::default(), }) } From cf47a65916fd4fb5c996946ffd579fd6755d02f7 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Tue, 19 Dec 2023 23:50:02 +0530 Subject: [PATCH 234/443] fix(events): add logger for incoming webhook payload (#3171) --- crates/router/src/routes/webhooks.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/router/src/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs index 2162ee561213..10eb4ef75e4d 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -25,8 +25,8 @@ pub async fn receive_incoming_webhook( flow.clone(), state, &req, - (), - |state, auth, _| { + WebhookBytes(body), + |state, auth, payload| { webhooks::webhooks_wrapper::( &flow, state.to_owned(), @@ -34,7 +34,7 @@ pub async fn receive_incoming_webhook( auth.merchant_account, auth.key_store, &connector_id_or_name, - body.clone(), + payload.0, ) }, &auth::MerchantIdAuth(merchant_id), @@ -42,3 +42,19 @@ pub async fn receive_incoming_webhook( )) .await } + +#[derive(Debug)] +struct WebhookBytes(web::Bytes); + +impl serde::Serialize for WebhookBytes { + fn serialize(&self, serializer: S) -> Result { + let payload: serde_json::Value = serde_json::from_slice(&self.0).unwrap_or_default(); + payload.serialize(serializer) + } +} + +impl common_utils::events::ApiEventMetric for WebhookBytes { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} From b98e53d5cba5a5af04ada9bd83fa7bd2e27462d9 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:52:02 +0530 Subject: [PATCH 235/443] refactor(payment_methods): make the card_holder_name as an empty string if not sent (#3173) --- crates/router/src/connector/aci/transformers.rs | 2 +- crates/router/src/connector/bambora/transformers.rs | 4 ++-- .../src/connector/braintree/braintree_graphql_transformers.rs | 2 +- crates/router/src/connector/dlocal/transformers.rs | 4 ++-- crates/router/src/connector/dummyconnector/transformers.rs | 3 +-- crates/router/src/connector/forte/transformers.rs | 2 +- crates/router/src/connector/mollie/transformers.rs | 2 +- crates/router/src/connector/noon/transformers.rs | 2 +- crates/router/src/connector/opayo/transformers.rs | 2 +- crates/router/src/connector/payeezy/transformers.rs | 2 +- crates/router/src/connector/paypal/transformers.rs | 2 +- crates/router/src/connector/powertranz/transformers.rs | 2 +- crates/router/src/connector/rapyd/transformers.rs | 4 ++-- crates/router/src/connector/shift4/transformers.rs | 2 +- crates/router/src/connector/stax/transformers.rs | 2 +- crates/router/src/connector/worldline/transformers.rs | 2 +- 16 files changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 470c6705e1f0..e729eacf9d99 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -256,7 +256,7 @@ impl TryFrom for PaymentDetails { card_number: card_data.card_number, card_holder: card_data .card_holder_name - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), card_expiry_month: card_data.card_exp_month, card_expiry_year: card_data.card_exp_year, card_cvv: card_data.card_cvc, diff --git a/crates/router/src/connector/bambora/transformers.rs b/crates/router/src/connector/bambora/transformers.rs index 4729bfa5a6ef..8f18ca272ddf 100644 --- a/crates/router/src/connector/bambora/transformers.rs +++ b/crates/router/src/connector/bambora/transformers.rs @@ -5,7 +5,7 @@ use masking::{PeekInterface, Secret}; use serde::{Deserialize, Deserializer, Serialize}; use crate::{ - connector::utils::{self, BrowserInformationData, PaymentsAuthorizeRequestData}, + connector::utils::{BrowserInformationData, PaymentsAuthorizeRequestData}, consts, core::errors, services, @@ -119,7 +119,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BamboraPaymentsRequest { let bambora_card = BamboraCard { name: req_card .card_holder_name - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index d1201309637a..9bdbd4392f7c 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -869,7 +869,7 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { cvv: card_data.card_cvc, cardholder_name: card_data .card_holder_name - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), }, }; Ok(Self { diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 25462c758f17..5eb682d4cfa4 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils::{self, AddressDetailsData, PaymentsAuthorizeRequestData, RouterData}, + connector::utils::{AddressDetailsData, PaymentsAuthorizeRequestData, RouterData}, core::errors, services, types::{self, api, storage::enums}, @@ -128,7 +128,7 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP holder_name: ccard .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), number: ccard.card_number.clone(), cvv: ccard.card_cvc.clone(), expiration_month: ccard.card_exp_month.clone(), diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs index bbb3b10c8e00..5c20d353583e 100644 --- a/crates/router/src/connector/dummyconnector/transformers.rs +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - connector::utils, core::errors, services, types::{self, api, storage::enums}, @@ -90,7 +89,7 @@ impl TryFrom for DummyConnectorCard { Ok(Self { name: value .card_holder_name - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), number: value.card_number, expiry_month: value.card_exp_month, expiry_year: value.card_exp_year, diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index ca7a66a9ffa7..4c770eb788d4 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -83,7 +83,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { name_on_card: ccard .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), account_number: ccard.card_number.clone(), expire_month: ccard.card_exp_month.clone(), expire_year: ccard.card_exp_year.clone(), diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index c1151adcf6db..5960e9cdb8d9 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -289,7 +289,7 @@ impl TryFrom<&types::TokenizationRouterData> for MollieCardTokenRequest { let card_holder = ccard .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?; + .unwrap_or(Secret::new("".to_string())); let card_number = ccard.card_number.clone(); let card_expiry_date = ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()); diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index e2262e7b8959..bbf284848b59 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -203,7 +203,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { name_on_card: req_card .card_holder_name .clone() - .ok_or_else(conn_utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), number_plain: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), expiry_year: req_card.get_expiry_year_4_digit(), diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index a0e3877f82b7..94ab62b4f7f1 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -31,7 +31,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { let card = OpayoCard { name: req_card .card_holder_name - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), number: req_card.card_number, expiry_month: req_card.card_exp_month, expiry_year: req_card.card_exp_year, diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 8b4f4a469596..7ae7feba68af 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -244,7 +244,7 @@ fn get_payment_method_data( cardholder_name: card .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), card_number: card.card_number.clone(), exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), cvv: card.card_cvc.clone(), diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 0871bc5097af..9f7a35989943 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -446,7 +446,7 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP name: ccard .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), number: Some(ccard.card_number.clone()), security_code: Some(ccard.card_cvc.clone()), attributes, diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 6d5a756c571b..005496332008 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -218,7 +218,7 @@ impl TryFrom<&Card> for Source { cardholder_name: card .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), card_pan: card.card_number.clone(), card_expiration: card.get_expiry_date_as_yymm(), card_cvv: card.card_cvc.clone(), diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index aab47bc8b210..9fd664748e38 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -4,7 +4,7 @@ use time::PrimitiveDateTime; use url::Url; use crate::{ - connector::utils::{self, PaymentsAuthorizeRequestData}, + connector::utils::PaymentsAuthorizeRequestData, consts, core::errors, pii::Secret, @@ -134,7 +134,7 @@ impl TryFrom<&RapydRouterData<&types::PaymentsAuthorizeRouterData>> for RapydPay name: ccard .card_holder_name .to_owned() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), cvv: ccard.card_cvc.to_owned(), }), address: None, diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 2b89e7ebf6c0..949082f4253a 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -299,7 +299,7 @@ impl cardholder_name: card .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), }; if item.is_three_ds() { Ok(Self::Cards3DSRequest(Box::new(Cards3DSRequest { diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 7395172239e8..081be000cf6c 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -228,7 +228,7 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), person_name: card_data .card_holder_name - .ok_or_else(missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), card_number: card_data.card_number, card_cvv: card_data.card_cvc, customer_id: Secret::new(customer_id), diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index d9e9d1ff7c09..b657756b6a86 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -354,7 +354,7 @@ fn make_card_request( cardholder_name: ccard .card_holder_name .clone() - .ok_or_else(utils::missing_field_err("card_holder_name"))?, + .unwrap_or(Secret::new("".to_string())), cvv: ccard.card_cvc.clone(), expiry_date, }; From e7949c23b9be56a4cd763d4990c1a95c0fefae95 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:24:58 +0530 Subject: [PATCH 236/443] refactor(core): fix payment status for 4xx (#3177) --- crates/router/src/core/payments/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 341699c09251..2cd62fbd4914 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -953,7 +953,7 @@ pub fn payment_attempt_status_fsm( ) -> storage_enums::AttemptStatus { match payment_method_data { Some(_) => match confirm { - Some(true) => storage_enums::AttemptStatus::Pending, + Some(true) => storage_enums::AttemptStatus::PaymentMethodAwaited, _ => storage_enums::AttemptStatus::ConfirmationAwaited, }, None => storage_enums::AttemptStatus::PaymentMethodAwaited, @@ -966,7 +966,7 @@ pub fn payment_intent_status_fsm( ) -> storage_enums::IntentStatus { match payment_method_data { Some(_) => match confirm { - Some(true) => storage_enums::IntentStatus::RequiresCustomerAction, + Some(true) => storage_enums::IntentStatus::RequiresPaymentMethod, _ => storage_enums::IntentStatus::RequiresConfirmation, }, None => storage_enums::IntentStatus::RequiresPaymentMethod, From 1d80949bef1228bf432dc445eaba15afccb030bd Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:17:28 +0530 Subject: [PATCH 237/443] feat(connector): [BOA] Handle BOA 5XX errors (#3178) --- crates/router/src/connector/bankofamerica.rs | 69 +++++++++++++++++++ .../connector/bankofamerica/transformers.rs | 17 +++++ .../payments/operations/payment_response.rs | 30 ++++---- 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 8bee764b1f72..18af4bda92e3 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -385,6 +385,33 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -546,6 +573,27 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -640,6 +688,27 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index e7c0c7a579b4..deca1dc6744d 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -626,6 +626,7 @@ impl attempt_status: None, connector_transaction_id: None, }), + status: enums::AttemptStatus::Failure, ..item.data }), } @@ -1024,6 +1025,22 @@ pub struct BankOfAmericaStandardErrorResponse { pub details: Option>, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaServerErrorResponse { + pub status: Option, + pub message: Option, + pub reason: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Reason { + SystemError, + ServerTimeout, + ServiceTimeout, +} + #[derive(Debug, Deserialize)] pub struct BankOfAmericaAuthenticationErrorResponse { pub response: AuthenticationErrorInformation, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 8b301c525fd7..adecf1b78ebe 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -471,20 +471,26 @@ async fn payment_response_update_tracker( flow_name.clone(), ) .await; - let status = + let status = match err.attempt_status { + // Use the status sent by connector in error_response if it's present + Some(status) => status, + None => // mark previous attempt status for technical failures in PSync flow - if flow_name == "PSync" { - match err.status_code { - // marking failure for 2xx because this is genuine payment failure - 200..=299 => storage::enums::AttemptStatus::Failure, - _ => router_data.status, - } - } else { - match err.status_code { - 500..=511 => storage::enums::AttemptStatus::Pending, - _ => storage::enums::AttemptStatus::Failure, + { + if flow_name == "PSync" { + match err.status_code { + // marking failure for 2xx because this is genuine payment failure + 200..=299 => storage::enums::AttemptStatus::Failure, + _ => router_data.status, + } + } else { + match err.status_code { + 500..=511 => storage::enums::AttemptStatus::Pending, + _ => storage::enums::AttemptStatus::Failure, + } } - }; + } + }; ( None, Some(storage::PaymentAttemptUpdate::ErrorUpdate { From 6890e9029d90bfd518ba23979a0bd507853dc983 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:11:45 +0000 Subject: [PATCH 238/443] test(postman): update postman collection files --- .../prophetpay.postman_collection.json | 1418 +++++++++++++++++ 1 file changed, 1418 insertions(+) create mode 100644 postman/collection-json/prophetpay.postman_collection.json diff --git a/postman/collection-json/prophetpay.postman_collection.json b/postman/collection-json/prophetpay.postman_collection.json new file mode 100644 index 000000000000..bd946301f55a --- /dev/null +++ b/postman/collection-json/prophetpay.postman_collection.json @@ -0,0 +1,1418 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"prophetpay\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"business_country\":\"US\",\"business_label\":\"default\",\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"card_redirect\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":true,\"amount_to_capture\":10000,\"business_country\":\"US\",\"customer_id\":\"not_a_rick_roll\",\"return_url\":\"https://www.google.com\",\"payment_method\":\"card_redirect\",\"payment_method_type\":\"card_redirect\",\"payment_method_data\":{\"card_redirect\":{\"card_redirect\":{}}},\"routing\":{\"type\":\"single\",\"data\":\"prophetpay\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":true,\"amount_to_capture\":8000,\"business_country\":\"US\",\"customer_id\":\"not_a_rick_roll\",\"return_url\":\"https://www.google.com\",\"payment_method\":\"card_redirect\",\"payment_method_type\":\"card_redirect\",\"payment_method_data\":{\"card_redirect\":{\"card_redirect\":{}}},\"routing\":{\"type\":\"single\",\"data\":\"prophetpay\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"amount_to_capture\":8000,\"business_country\":\"US\",\"customer_id\":\"not_a_rick_roll\",\"return_url\":\"https://www.google.com\",\"routing\":{\"type\":\"single\",\"data\":\"prophetpay\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card_redirect\",\"payment_method_type\":\"card_redirect\",\"payment_method_data\":{\"card_redirect\":{\"card_redirect\":{}}},\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "info": { + "_postman_id": "a553df38-fa33-4522-b029-1cd32821730e", + "name": "Prophetpay Postman Collection", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "24206034" + }, + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + } + ] +} From de366b35f9c5e8439d86c58839dc2f79e39e9c9b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:11:45 +0000 Subject: [PATCH 239/443] chore(version): v1.103.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2c441578df..6784fd3fdeba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.103.0 (2023-12-20) + +### Features + +- **connector:** + - [NMI] Implement webhook for Payments and Refunds ([#3164](https://github.com/juspay/hyperswitch/pull/3164)) ([`30c1401`](https://github.com/juspay/hyperswitch/commit/30c14019d067ad5f105563f205eb1941010233e8)) + - [BOA] Handle BOA 5XX errors ([#3178](https://github.com/juspay/hyperswitch/pull/3178)) ([`1d80949`](https://github.com/juspay/hyperswitch/commit/1d80949bef1228bf432dc445eaba15afccb030bd)) +- **connector-config:** Add wasm support for dashboard connector configuration ([#3138](https://github.com/juspay/hyperswitch/pull/3138)) ([`b0ffbe9`](https://github.com/juspay/hyperswitch/commit/b0ffbe9355b7e38226994c1ccbbe80cdbc77adde)) +- **db:** Implement `AuthorizationInterface` for `MockDb` ([#3151](https://github.com/juspay/hyperswitch/pull/3151)) ([`396a64f`](https://github.com/juspay/hyperswitch/commit/396a64f3bbad6e75d4b263286a7ef6a2f09b180e)) +- **postman:** [Prophetpay] Add test cases ([#2946](https://github.com/juspay/hyperswitch/pull/2946)) ([`583d7b8`](https://github.com/juspay/hyperswitch/commit/583d7b87a711102e4e62417f3191ac837886eca9)) + +### Bug Fixes + +- **connector:** + - [NMI] Fix response deserialization for vault id creation ([#3166](https://github.com/juspay/hyperswitch/pull/3166)) ([`d44daaf`](https://github.com/juspay/hyperswitch/commit/d44daaf539021a9cbc33c9391172c38825d74dcd)) + - Connector wise validation for zero auth flow ([#3159](https://github.com/juspay/hyperswitch/pull/3159)) ([`45ba128`](https://github.com/juspay/hyperswitch/commit/45ba128b6ab39f513dd114567d9915acf0eaea20)) +- **events:** Add logger for incoming webhook payload ([#3171](https://github.com/juspay/hyperswitch/pull/3171)) ([`cf47a65`](https://github.com/juspay/hyperswitch/commit/cf47a65916fd4fb5c996946ffd579fd6755d02f7)) +- **users:** Send correct `user_role` values in `switch_merchant` response ([#3167](https://github.com/juspay/hyperswitch/pull/3167)) ([`dc589d5`](https://github.com/juspay/hyperswitch/commit/dc589d580f1382874bc755d3719bd3244fdedc67)) + +### Refactors + +- **core:** Fix payment status for 4xx ([#3177](https://github.com/juspay/hyperswitch/pull/3177)) ([`e7949c2`](https://github.com/juspay/hyperswitch/commit/e7949c23b9be56a4cd763d4990c1a95c0fefae95)) +- **payment_methods:** Make the card_holder_name as an empty string if not sent ([#3173](https://github.com/juspay/hyperswitch/pull/3173)) ([`b98e53d`](https://github.com/juspay/hyperswitch/commit/b98e53d5cba5a5af04ada9bd83fa7bd2e27462d9)) + +### Testing + +- **postman:** Update postman collection files ([`6890e90`](https://github.com/juspay/hyperswitch/commit/6890e9029d90bfd518ba23979a0bd507853dc983)) + +### Documentation + +- **connector:** Update connector integration documentation ([#3041](https://github.com/juspay/hyperswitch/pull/3041)) ([`ce5514e`](https://github.com/juspay/hyperswitch/commit/ce5514eadfce240bc4cefb472405f37432a8507b)) + +**Full Changelog:** [`v1.102.1...v1.103.0`](https://github.com/juspay/hyperswitch/compare/v1.102.1...v1.103.0) + +- - - + + ## 1.102.1 (2023-12-18) ### Bug Fixes From a5e141b542622e7065f0e0070a3cddacde78fd8a Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:15:56 +0530 Subject: [PATCH 240/443] fix(connector): Remove set_body method for connectors implementing default get_request_body (#3182) --- crates/router/src/connector/aci.rs | 3 --- crates/router/src/connector/airwallex.rs | 6 ------ crates/router/src/connector/bambora.rs | 3 --- crates/router/src/connector/bankofamerica.rs | 3 --- crates/router/src/connector/checkout.rs | 6 ------ crates/router/src/connector/dummyconnector.rs | 3 --- crates/router/src/connector/globalpay.rs | 3 --- crates/router/src/connector/helcim.rs | 3 --- crates/router/src/connector/iatapay.rs | 3 --- crates/router/src/connector/multisafepay.rs | 3 --- crates/router/src/connector/noon.rs | 3 --- crates/router/src/connector/opayo.rs | 3 --- crates/router/src/connector/paypal.rs | 3 --- crates/router/src/connector/shift4.rs | 3 --- crates/router/src/connector/stripe.rs | 6 ------ crates/router/src/connector/worldpay.rs | 6 ------ crates/router/src/connector/zen.rs | 3 --- 17 files changed, 63 deletions(-) diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 5c65a8a2726d..bbb88209b273 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -214,9 +214,6 @@ impl .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .set_body(types::PaymentsSyncType::get_request_body( - self, req, connectors, - )?) .build(), )) } diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index 4fc813d628ec..19d69b688c9a 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -197,9 +197,6 @@ impl ConnectorIntegration ConnectorIntegration Date: Thu, 21 Dec 2023 12:33:36 +0530 Subject: [PATCH 241/443] fix(connector): [Paypal] remove shipping address as mandatory field for paypal wallet (#3181) --- crates/router/src/connector/paypal/transformers.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 9f7a35989943..2d3292359d38 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -486,7 +486,12 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP experience_context: ContextStruct { return_url: item.router_data.request.complete_authorize_url.clone(), cancel_url: item.router_data.request.complete_authorize_url.clone(), - shipping_preference: ShippingPreference::SetProvidedAddress, + shipping_preference: if item.router_data.address.shipping.is_some() + { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, user_action: Some(UserAction::PayNow), }, })); From 9852dac1fe5b223edaa7357c273dcc791c133928 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 07:27:07 +0000 Subject: [PATCH 242/443] chore(version): v1.103.1 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6784fd3fdeba..8010277b5659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.103.1 (2023-12-21) + +### Bug Fixes + +- **connector:** + - Remove set_body method for connectors implementing default get_request_body ([#3182](https://github.com/juspay/hyperswitch/pull/3182)) ([`a5e141b`](https://github.com/juspay/hyperswitch/commit/a5e141b542622e7065f0e0070a3cddacde78fd8a)) + - [Paypal] remove shipping address as mandatory field for paypal wallet ([#3181](https://github.com/juspay/hyperswitch/pull/3181)) ([`680ed60`](https://github.com/juspay/hyperswitch/commit/680ed603c5113ec29fbd13c4c633e18ad4ad10ee)) + +**Full Changelog:** [`v1.103.0...v1.103.1`](https://github.com/juspay/hyperswitch/compare/v1.103.0...v1.103.1) + +- - - + + ## 1.103.0 (2023-12-20) ### Features From c51c761677e8c5ff80de40f8796f340cf1331f96 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:44:37 +0530 Subject: [PATCH 243/443] fix(connector): [Trustpay] Use `connector_request_reference_id` for merchant reference instead of `payment_id` (#2885) --- crates/router/src/connector/trustpay/transformers.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index c112b6440178..cca0c2d9893d 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -299,7 +299,7 @@ fn get_card_request_data( cvv: ccard.card_cvc.clone(), expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), cardholder: get_full_name(params.billing_first_name, billing_last_name), - reference: item.payment_id.clone(), + reference: item.connector_request_reference_id.clone(), redirect_url: return_url, billing_city: params.billing_city, billing_country: params.billing_country, @@ -377,7 +377,7 @@ fn get_bank_redirection_request_data( currency: item.request.currency.to_string(), }, references: References { - merchant_reference: item.payment_id.clone(), + merchant_reference: item.connector_request_reference_id.clone(), }, debtor: get_debtor_info(item, pm, params)?, }, @@ -1014,7 +1014,7 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsPreProcessingRouterData>> currency: currency.to_string(), init_apple_pay: is_apple_pay, init_google_pay: is_google_pay, - reference: item.router_data.payment_id.clone(), + reference: item.router_data.connector_request_reference_id.clone(), }) } } From 15987cc81ecba3c1d0de4fa0a12424066a8842eb Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:26:32 +0530 Subject: [PATCH 244/443] feat(router): make the billing country for apple pay as optional field (#3188) --- crates/api_models/src/payments.rs | 2 +- crates/router/src/connector/bluesnap/transformers.rs | 2 +- crates/router/src/connector/payme/transformers.rs | 9 ++++++++- crates/router/src/connector/trustpay/transformers.rs | 2 +- crates/router/src/core/payments/flows/session_flow.rs | 8 +------- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 5efebb14f819..d2df44a9b21a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3004,7 +3004,7 @@ pub struct SecretInfoToInitiateSdk { pub struct ApplePayPaymentRequest { /// The code for country #[schema(value_type = CountryAlpha2, example = "US")] - pub country_code: api_enums::CountryAlpha2, + pub country_code: Option, /// The code for currency #[schema(value_type = Currency, example = "USD")] pub currency_code: api_enums::Currency, diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index 3a980aee8199..17cdf3b519bb 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -529,7 +529,7 @@ impl TryFrom } _ => { let currency_code = item.data.request.get_currency()?; + let country_code = item + .data + .address + .billing + .as_ref() + .and_then(|billing| billing.address.as_ref()) + .and_then(|address| address.country); let amount = item.data.request.get_amount()?; let amount_in_base_unit = utils::to_currency_base_unit(amount, currency_code)?; let pmd = item.data.request.payment_method_data.to_owned(); @@ -559,7 +566,7 @@ impl api_models::payments::ApplePaySessionResponse::NoSessionResponse, payment_request_data: Some( api_models::payments::ApplePayPaymentRequest { - country_code: item.data.get_billing_country()?, + country_code, currency_code, total: api_models::payments::AmountInfo { label: "Apple Pay".to_string(), diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index cca0c2d9893d..8ae12622fb06 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -1181,7 +1181,7 @@ pub fn get_apple_pay_session( }, ), payment_request_data: Some(api_models::payments::ApplePayPaymentRequest { - country_code: apple_pay_init_result.country_code, + country_code: Some(apple_pay_init_result.country_code), currency_code: apple_pay_init_result.currency_code, supported_networks: Some(apple_pay_init_result.supported_networks.clone()), merchant_capabilities: Some( diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index 91513019179f..de697e02f780 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -371,13 +371,7 @@ fn get_apple_pay_payment_request( merchant_identifier: &str, ) -> RouterResult { let applepay_payment_request = payment_types::ApplePayPaymentRequest { - country_code: session_data - .country - .to_owned() - .get_required_value("country_code") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "country_code", - })?, + country_code: session_data.country, currency_code: session_data.currency, total: amount_info, merchant_capabilities: Some(payment_request_data.merchant_capabilities), From b06a8d6e0d7fc4fb1bec30f702d64f0bd5e1068e Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 21 Dec 2023 21:06:59 +0530 Subject: [PATCH 245/443] fix(users): wrong `user_role` insertion in `invite_user` for new users (#3193) --- crates/router/src/core/user.rs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index a13eba6ed5b5..6fb76bd75b3b 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,4 +1,6 @@ use api_models::user as user_api; +#[cfg(feature = "email")] +use diesel_models::user_role::UserRoleNew; use diesel_models::{enums::UserStatus, user as storage_user}; #[cfg(feature = "email")] use error_stack::IntoReport; @@ -369,7 +371,6 @@ pub async fn invite_user( let invitee_user_from_db = domain::UserFromStorage::from(invitee_user); let now = common_utils::date_time::now(); - use diesel_models::user_role::UserRoleNew; state .store .insert_user_role(UserRoleNew { @@ -401,17 +402,35 @@ pub async fn invite_user( .err() .unwrap_or(false) { - let new_user = domain::NewUser::try_from((request.clone(), user_from_token))?; + let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?; new_user .insert_user_in_db(state.store.as_ref()) .await .change_context(UserErrors::InternalServerError)?; - new_user - .clone() - .insert_user_role_in_db(state.clone(), request.role_id, UserStatus::InvitationSent) + + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: new_user.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id, + role_id: request.role_id, + org_id: user_from_token.org_id, + status: UserStatus::InvitationSent, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id, + created_at: now, + last_modified: now, + }) .await - .change_context(UserErrors::InternalServerError)?; + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; let email_contents = email_types::InviteUser { recipient_email: invitee_email, From 25fd3d502e48f10dd3acbdc88caea4007310d4ee Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:11:57 +0530 Subject: [PATCH 246/443] feat(connector): [BOA] Implement apple pay manual flow (#3191) --- .../connector/bankofamerica/transformers.rs | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index deca1dc6744d..2c91166fd567 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -111,6 +111,19 @@ pub struct GooglePayPaymentInformation { fluid_data: FluidData, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenizedCard { + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenPaymentInformation { + fluid_data: FluidData, + tokenized_card: ApplePayTokenizedCard, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApplePayPaymentInformation { @@ -123,6 +136,7 @@ pub enum PaymentInformation { Cards(CardPaymentInformation), GooglePay(GooglePayPaymentInformation), ApplePay(ApplePayPaymentInformation), + ApplePayToken(ApplePayTokenPaymentInformation), } #[derive(Debug, Serialize)] @@ -434,14 +448,42 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> match item.router_data.request.payment_method_data.clone() { payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - payments::WalletData::ApplePay(_) => { - let payment_method_token = item.router_data.get_payment_method_token()?; - match payment_method_token { - types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { - Self::try_from((item, decrypt_data)) - } - types::PaymentMethodToken::Token(_) => { - Err(errors::ConnectorError::InvalidWalletToken)? + payments::WalletData::ApplePay(apple_pay_data) => { + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, + None => { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + let processing_information = ProcessingInformation::from(( + item, + Some(PaymentSolution::ApplePay), + )); + let client_reference_information = + ClientReferenceInformation::from(item); + let payment_information = PaymentInformation::ApplePayToken( + ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }, + ); + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + }) } } } From 79a18e2bf7bb1f338cf982fb1a152add2ed4e087 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:12:10 +0530 Subject: [PATCH 247/443] fix(connector): [BOA/Cyb] Truncate state length to <20 (#3198) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/bankofamerica/transformers.rs | 6 ++++-- crates/router/src/connector/cybersource/transformers.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 2c91166fd567..ab9e067a932a 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,7 +1,7 @@ use api_models::payments; use base64::Engine; use common_utils::pii; -use masking::Secret; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ @@ -202,12 +202,14 @@ fn build_bill_to( .address .as_ref() .ok_or_else(utils::missing_field_err("billing.address"))?; + let mut state = address.to_state_code()?.peek().clone(); + state.truncate(20); Ok(BillTo { first_name: address.get_first_name()?.to_owned(), last_name: address.get_last_name()?.to_owned(), address1: address.get_line1()?.to_owned(), locality: address.get_city()?.to_owned(), - administrative_area: address.to_state_code()?, + administrative_area: Secret::from(state), postal_code: address.get_zip()?.to_owned(), country: address.get_country()?.to_owned(), email, diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index f99c90989d0c..bc4e7509519f 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,7 +1,7 @@ use api_models::payments; use base64::Engine; use common_utils::pii; -use masking::Secret; +use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ @@ -427,12 +427,14 @@ fn build_bill_to( .address .as_ref() .ok_or_else(utils::missing_field_err("billing.address"))?; + let mut state = address.to_state_code()?.peek().clone(); + state.truncate(20); Ok(BillTo { first_name: address.get_first_name()?.to_owned(), last_name: address.get_last_name()?.to_owned(), address1: address.get_line1()?.to_owned(), locality: address.get_city()?.to_owned(), - administrative_area: address.to_state_code()?, + administrative_area: Secret::from(state), postal_code: address.get_zip()?.to_owned(), country: address.get_country()?.to_owned(), email, From 716a74cf8449583541c426a5c427c9e32f5b2528 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 22 Dec 2023 17:28:44 +0530 Subject: [PATCH 248/443] fix(connector): [Iatapay] fix error response handling when payment is failed (#3197) --- .../src/connector/iatapay/transformers.rs | 110 ++++++++++++------ 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index b6d2dee4a01b..e6ecc6da2ffe 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -1,16 +1,20 @@ use std::collections::HashMap; use api_models::enums::PaymentMethod; +use common_utils::errors::CustomResult; use masking::{Secret, SwitchStrategy}; use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{self, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData}, + consts, core::errors, services, types::{self, api, storage::enums, PaymentsAuthorizeData}, }; +type Error = error_stack::Report; + // Every access token will be valid for 5 minutes. It contains grant_type and scope for different type of access, but for our usecases it should be only 'client_credentials' and 'payment' resp(as per doc) for all type of api call. #[derive(Debug, Serialize)] pub struct IatapayAuthUpdateRequest { @@ -257,53 +261,85 @@ pub struct IatapayPaymentsResponse { pub bank_transfer_description: Option, pub checkout_methods: Option, pub failure_code: Option, + pub failure_details: Option, +} + +fn get_iatpay_response( + response: IatapayPaymentsResponse, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let status = enums::AttemptStatus::from(response.status); + let error = if status == enums::AttemptStatus::Failure { + Some(types::ErrorResponse { + code: response + .failure_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .failure_details + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.failure_details, + status_code, + attempt_status: Some(status), + connector_transaction_id: response.iata_payment_id.clone(), + }) + } else { + None + }; + let form_fields = HashMap::new(); + let id = match response.iata_payment_id.clone() { + Some(s) => types::ResponseId::ConnectorTransactionId(s), + None => types::ResponseId::NoResponseId, + }; + let connector_response_reference_id = response.merchant_payment_id.or(response.iata_payment_id); + + let payment_response_data = response.checkout_methods.map_or( + types::PaymentsResponseData::TransactionResponse { + resource_id: id.clone(), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, + }, + |checkout_methods| types::PaymentsResponseData::TransactionResponse { + resource_id: id, + redirection_data: Some(services::RedirectForm::Form { + endpoint: checkout_methods.redirect.redirect_url, + method: services::Method::Get, + form_fields, + }), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: connector_response_reference_id.clone(), + incremental_authorization_allowed: None, + }, + ); + Ok((status, error, payment_response_data)) } impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: types::ResponseRouterData, ) -> Result { - let form_fields = HashMap::new(); - let id = match item.response.iata_payment_id.clone() { - Some(s) => types::ResponseId::ConnectorTransactionId(s), - None => types::ResponseId::NoResponseId, - }; - let connector_response_reference_id = item - .response - .merchant_payment_id - .or(item.response.iata_payment_id); + let (status, error, payment_response_data) = + get_iatpay_response(item.response, item.http_code)?; Ok(Self { - status: enums::AttemptStatus::from(item.response.status), - response: item.response.checkout_methods.map_or( - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: id.clone(), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: connector_response_reference_id.clone(), - incremental_authorization_allowed: None, - }), - |checkout_methods| { - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: id, - redirection_data: Some(services::RedirectForm::Form { - endpoint: checkout_methods.redirect.redirect_url, - method: services::Method::Get, - form_fields, - }), - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: connector_response_reference_id.clone(), - incremental_authorization_allowed: None, - }) - }, - ), + status, + response: error.map_or_else(|| Ok(payment_response_data), Err), ..item.data }) } From 07fd9bedf02a1d70fc248fbbab480a5e24a7f077 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 22 Dec 2023 19:38:58 +0530 Subject: [PATCH 249/443] fix(connector): [BOA] Display 2XX Failure Errors (#3200) --- .../connector/bankofamerica/transformers.rs | 255 ++++++++++++------ 1 file changed, 172 insertions(+), 83 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index ab9e067a932a..bae9d43da22d 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -309,7 +309,7 @@ impl From<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, @@ -541,7 +541,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> } } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BankofamericaPaymentStatus { Authorized, @@ -594,12 +594,13 @@ pub enum BankOfAmericaPaymentsResponse { ErrorInformation(BankOfAmericaErrorInformationResponse), } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BankOfAmericaClientReferenceResponse { id: String, status: BankofamericaPaymentStatus, client_reference_information: ClientReferenceInformation, + error_information: Option, } #[derive(Debug, Deserialize)] @@ -609,12 +610,101 @@ pub struct BankOfAmericaErrorInformationResponse { error_information: BankOfAmericaErrorInformation, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct BankOfAmericaErrorInformation { reason: Option, message: Option, } +fn get_error_response_if_failure( + (info_response, status, http_code): ( + &BankOfAmericaClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Option { + if is_payment_failure(status) { + let (message, reason) = match info_response.error_information.as_ref() { + Some(error_info) => ( + error_info + .message + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + error_info.reason.clone(), + ), + None => (consts::NO_ERROR_MESSAGE.to_string(), None), + }; + + Some(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message, + reason, + status_code: http_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: Some(info_response.id.clone()), + }) + } else { + None + } +} + +fn is_payment_failure(status: enums::AttemptStatus) -> bool { + match status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::Failure => true, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => false, + } +} + +fn get_payment_response( + (info_response, status, http_code): ( + &BankOfAmericaClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Result { + let error_response = get_error_response_if_failure((info_response, status, http_code)); + match error_response { + Some(error) => Err(error), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .clone() + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed: None, + }), + } +} + impl TryFrom< types::ResponseRouterData< @@ -635,29 +725,18 @@ impl >, ) -> Result { match item.response { - BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { - status: enums::AttemptStatus::foreign_from(( - info_response.status, + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), item.data.request.is_auto_capture()?, - )), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - info_response.id.clone(), - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some( - info_response - .client_reference_information - .code - .unwrap_or(info_response.id), - ), - incremental_authorization_allowed: None, - }), - ..item.data - }), + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), @@ -697,26 +776,16 @@ impl >, ) -> Result { match item.response { - BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { - status: enums::AttemptStatus::foreign_from((info_response.status, true)), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - info_response.id.clone(), - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some( - info_response - .client_reference_information - .code - .unwrap_or(info_response.id), - ), - incremental_authorization_allowed: None, - }), - ..item.data - }), + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), true)); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), @@ -755,26 +824,16 @@ impl >, ) -> Result { match item.response { - BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => Ok(Self { - status: enums::AttemptStatus::foreign_from((info_response.status, false)), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - info_response.id.clone(), - ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some( - info_response - .client_reference_information - .code - .unwrap_or(info_response.id), - ), - incremental_authorization_allowed: None, - }), - ..item.data - }), + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), false)); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), @@ -806,6 +865,7 @@ pub struct BankOfAmericaApplicationInfoResponse { id: String, application_information: ApplicationInformation, client_reference_information: Option, + error_information: Option, } #[derive(Debug, Deserialize)] @@ -834,25 +894,54 @@ impl >, ) -> Result { match item.response { - BankOfAmericaTransactionResponse::ApplicationInformation(app_response) => Ok(Self { - status: enums::AttemptStatus::foreign_from(( + BankOfAmericaTransactionResponse::ApplicationInformation(app_response) => { + let status = enums::AttemptStatus::foreign_from(( app_response.application_information.status, item.data.request.is_auto_capture()?, - )), - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(app_response.id.clone()), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: app_response - .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(app_response.id)), - incremental_authorization_allowed: None, - }), - ..item.data - }), + )); + if is_payment_failure(status) { + let (message, reason) = match app_response.error_information { + Some(error_info) => ( + error_info + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + error_info.reason, + ), + None => (consts::NO_ERROR_MESSAGE.to_string(), None), + }; + Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message, + reason, + status_code: item.http_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: Some(app_response.id), + }), + status: enums::AttemptStatus::Failure, + ..item.data + }) + } else { + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + app_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: app_response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(app_response.id)), + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + } BankOfAmericaTransactionResponse::ErrorInformation(error_response) => Ok(Self { status: item.data.status, response: Ok(types::PaymentsResponseData::TransactionResponse { From 86c26221357e14b585f44c6ebe46962c085f6552 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 22 Dec 2023 20:16:58 +0530 Subject: [PATCH 250/443] fix(connector): [CYBERSOURCE] Display 2XX Failure Errors (#3201) --- .../src/connector/cybersource/transformers.rs | 233 ++++++++++++------ 1 file changed, 157 insertions(+), 76 deletions(-) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index bc4e7509519f..a19dc0d3c34a 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -842,13 +842,14 @@ pub enum CybersourcePaymentsResponse { ErrorInformation(CybersourceErrorInformationResponse), } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceClientReferenceResponse { id: String, status: CybersourcePaymentStatus, client_reference_information: ClientReferenceInformation, token_information: Option, + error_information: Option, } #[derive(Debug, Clone, Deserialize)] @@ -928,6 +929,107 @@ impl } } +fn get_error_response_if_failure( + (info_response, status, http_code): ( + &CybersourceClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Option { + if is_payment_failure(status) { + let (message, reason) = match info_response.error_information.as_ref() { + Some(error_info) => ( + error_info + .message + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + error_info.reason.clone(), + ), + None => (consts::NO_ERROR_MESSAGE.to_string(), None), + }; + + Some(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message, + reason, + status_code: http_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: Some(info_response.id.clone()), + }) + } else { + None + } +} + +fn is_payment_failure(status: enums::AttemptStatus) -> bool { + match status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::Failure => true, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => false, + } +} + +fn get_payment_response( + (info_response, status, http_code): ( + &CybersourceClientReferenceResponse, + enums::AttemptStatus, + u16, + ), +) -> Result { + let error_response = get_error_response_if_failure((info_response, status, http_code)); + match error_response { + Some(error) => Err(error), + None => { + let incremental_authorization_allowed = + Some(status == enums::AttemptStatus::Authorized); + let mandate_reference = + info_response + .token_information + .clone() + .map(|token_info| types::MandateReference { + connector_mandate_id: Some(token_info.instrument_identifier.id), + payment_method_id: None, + }); + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .clone() + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed, + }) + } + } +} + impl TryFrom< types::ResponseRouterData< @@ -950,35 +1052,13 @@ impl match item.response { CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { let status = enums::AttemptStatus::foreign_from(( - info_response.status, + info_response.status.clone(), item.data.request.is_auto_capture()?, )); - let incremental_authorization_allowed = - Some(status == enums::AttemptStatus::Authorized); - let mandate_reference = - info_response - .token_information - .map(|token_info| types::MandateReference { - connector_mandate_id: Some(token_info.instrument_identifier.id), - payment_method_id: None, - }); - let connector_response_reference_id = Some( - info_response - .client_reference_information - .code - .unwrap_or(info_response.id.clone()), - ); + let response = get_payment_response((&info_response, status, item.http_code)); Ok(Self { status, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(info_response.id), - redirection_data: None, - mandate_reference, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id, - incremental_authorization_allowed, - }), + response, ..item.data }) } @@ -1010,24 +1090,12 @@ impl ) -> Result { match item.response { CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { - let status = enums::AttemptStatus::foreign_from((info_response.status, true)); - let connector_response_reference_id = Some( - info_response - .client_reference_information - .code - .unwrap_or(info_response.id.clone()), - ); + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), true)); + let response = get_payment_response((&info_response, status, item.http_code)); Ok(Self { status, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(info_response.id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id, - incremental_authorization_allowed: None, - }), + response, ..item.data }) } @@ -1059,24 +1127,12 @@ impl ) -> Result { match item.response { CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { - let status = enums::AttemptStatus::foreign_from((info_response.status, false)); - let connector_response_reference_id = Some( - info_response - .client_reference_information - .code - .unwrap_or(info_response.id.clone()), - ); + let status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), false)); + let response = get_payment_response((&info_response, status, item.http_code)); Ok(Self { status, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(info_response.id), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id, - incremental_authorization_allowed: None, - }), + response, ..item.data }) } @@ -1215,9 +1271,10 @@ pub struct CybersourceApplicationInfoResponse { id: String, application_information: ApplicationInformation, client_reference_information: Option, + error_information: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApplicationInformation { status: CybersourcePaymentStatus, @@ -1250,24 +1307,48 @@ impl )); let incremental_authorization_allowed = Some(status == enums::AttemptStatus::Authorized); - Ok(Self { - status, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - app_response.id.clone(), + if is_payment_failure(status) { + let (message, reason) = match app_response.error_information { + Some(error_info) => ( + error_info + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + error_info.reason, ), - redirection_data: None, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - incremental_authorization_allowed, - connector_response_reference_id: app_response - .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(app_response.id)), - }), - ..item.data - }) + None => (consts::NO_ERROR_MESSAGE.to_string(), None), + }; + Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message, + reason, + status_code: item.http_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: Some(app_response.id), + }), + status: enums::AttemptStatus::Failure, + ..item.data + }) + } else { + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + app_response.id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: app_response + .client_reference_information + .map(|cref| cref.code) + .unwrap_or(Some(app_response.id)), + incremental_authorization_allowed, + }), + ..item.data + }) + } } CybersourceTransactionResponse::ErrorInformation(error_response) => Ok(Self { status: item.data.status, From 61cd8d50a863308b03b3c158bd493b4408c03627 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:11:39 +0000 Subject: [PATCH 251/443] chore(version): v1.104.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8010277b5659..a90b1c9bb86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.104.0 (2023-12-22) + +### Features + +- **connector:** [BOA] Implement apple pay manual flow ([#3191](https://github.com/juspay/hyperswitch/pull/3191)) ([`25fd3d5`](https://github.com/juspay/hyperswitch/commit/25fd3d502e48f10dd3acbdc88caea4007310d4ee)) +- **router:** Make the billing country for apple pay as optional field ([#3188](https://github.com/juspay/hyperswitch/pull/3188)) ([`15987cc`](https://github.com/juspay/hyperswitch/commit/15987cc81ecba3c1d0de4fa0a12424066a8842eb)) + +### Bug Fixes + +- **connector:** + - [Trustpay] Use `connector_request_reference_id` for merchant reference instead of `payment_id` ([#2885](https://github.com/juspay/hyperswitch/pull/2885)) ([`c51c761`](https://github.com/juspay/hyperswitch/commit/c51c761677e8c5ff80de40f8796f340cf1331f96)) + - [BOA/Cyb] Truncate state length to <20 ([#3198](https://github.com/juspay/hyperswitch/pull/3198)) ([`79a18e2`](https://github.com/juspay/hyperswitch/commit/79a18e2bf7bb1f338cf982fb1a152add2ed4e087)) + - [Iatapay] fix error response handling when payment is failed ([#3197](https://github.com/juspay/hyperswitch/pull/3197)) ([`716a74c`](https://github.com/juspay/hyperswitch/commit/716a74cf8449583541c426a5c427c9e32f5b2528)) + - [BOA] Display 2XX Failure Errors ([#3200](https://github.com/juspay/hyperswitch/pull/3200)) ([`07fd9be`](https://github.com/juspay/hyperswitch/commit/07fd9bedf02a1d70fc248fbbab480a5e24a7f077)) + - [CYBERSOURCE] Display 2XX Failure Errors ([#3201](https://github.com/juspay/hyperswitch/pull/3201)) ([`86c2622`](https://github.com/juspay/hyperswitch/commit/86c26221357e14b585f44c6ebe46962c085f6552)) +- **users:** Wrong `user_role` insertion in `invite_user` for new users ([#3193](https://github.com/juspay/hyperswitch/pull/3193)) ([`b06a8d6`](https://github.com/juspay/hyperswitch/commit/b06a8d6e0d7fc4fb1bec30f702d64f0bd5e1068e)) + +**Full Changelog:** [`v1.103.1...v1.104.0`](https://github.com/juspay/hyperswitch/compare/v1.103.1...v1.104.0) + +- - - + + ## 1.103.1 (2023-12-21) ### Bug Fixes From 110d3d211be2edf47533cc5297ae159cad0e5034 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:37:44 +0530 Subject: [PATCH 252/443] feat(connector): [BOA/CYBERSOURCE] Populate connector_transaction_id (#3202) --- .../connector/bankofamerica/transformers.rs | 25 +++++++++++++++---- .../src/connector/cybersource/transformers.rs | 21 ++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index bae9d43da22d..477bbec7350b 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -551,8 +551,16 @@ pub enum BankofamericaPaymentStatus { Reversed, Pending, Declined, + Rejected, + Challenge, AuthorizedPendingReview, + AuthorizedRiskDeclined, Transmitted, + InvalidRequest, + ServerError, + PendingAuthentication, + PendingReview, + //PartialAuthorized, not being consumed yet. } impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { @@ -580,8 +588,15 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { BankofamericaPaymentStatus::Voided | BankofamericaPaymentStatus::Reversed => { Self::Voided } - BankofamericaPaymentStatus::Failed | BankofamericaPaymentStatus::Declined => { - Self::Failure + BankofamericaPaymentStatus::Failed + | BankofamericaPaymentStatus::Declined + | BankofamericaPaymentStatus::AuthorizedRiskDeclined + | BankofamericaPaymentStatus::InvalidRequest + | BankofamericaPaymentStatus::Rejected + | BankofamericaPaymentStatus::ServerError => Self::Failure, + BankofamericaPaymentStatus::PendingAuthentication => Self::AuthenticationPending, + BankofamericaPaymentStatus::PendingReview | BankofamericaPaymentStatus::Challenge => { + Self::Pending } } } @@ -747,7 +762,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(error_response.id), }), status: enums::AttemptStatus::Failure, ..item.data @@ -796,7 +811,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(error_response.id), }), ..item.data }), @@ -844,7 +859,7 @@ impl reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(error_response.id), }), ..item.data }), diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index a19dc0d3c34a..1d828a95d60a 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -773,8 +773,16 @@ pub enum CybersourcePaymentStatus { Reversed, Pending, Declined, + Rejected, + Challenge, AuthorizedPendingReview, + AuthorizedRiskDeclined, Transmitted, + InvalidRequest, + ServerError, + PendingAuthentication, + PendingReview, + //PartialAuthorized, not being consumed yet. } #[derive(Debug, Clone, Deserialize)] @@ -808,7 +816,16 @@ impl ForeignFrom<(CybersourcePaymentStatus, bool)> for enums::AttemptStatus { Self::Charged } CybersourcePaymentStatus::Voided | CybersourcePaymentStatus::Reversed => Self::Voided, - CybersourcePaymentStatus::Failed | CybersourcePaymentStatus::Declined => Self::Failure, + CybersourcePaymentStatus::Failed + | CybersourcePaymentStatus::Declined + | CybersourcePaymentStatus::AuthorizedRiskDeclined + | CybersourcePaymentStatus::Rejected + | CybersourcePaymentStatus::InvalidRequest + | CybersourcePaymentStatus::ServerError => Self::Failure, + CybersourcePaymentStatus::PendingAuthentication => Self::AuthenticationPending, + CybersourcePaymentStatus::PendingReview | CybersourcePaymentStatus::Challenge => { + Self::Pending + } } } } @@ -922,7 +939,7 @@ impl reason: error_response.error_information.reason.clone(), status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(error_response.id.clone()), }), ..item.data } From c7e15aee9dd3d2c6bb035ba3517cc1f80ad6370d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:52:54 +0000 Subject: [PATCH 253/443] chore(version): v1.105.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a90b1c9bb86d..ebd711b2e605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.105.0 (2023-12-23) + +### Features + +- **connector:** [BOA/CYBERSOURCE] Populate connector_transaction_id ([#3202](https://github.com/juspay/hyperswitch/pull/3202)) ([`110d3d2`](https://github.com/juspay/hyperswitch/commit/110d3d211be2edf47533cc5297ae159cad0e5034)) + +**Full Changelog:** [`v1.104.0...v1.105.0`](https://github.com/juspay/hyperswitch/compare/v1.104.0...v1.105.0) + +- - - + + ## 1.104.0 (2023-12-22) ### Features From a46b8a7b05367fbbdbf4fca89d8a6b29110a4e1c Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 26 Dec 2023 17:36:33 +0530 Subject: [PATCH 254/443] refactor(connector): [NMI] Include mandatory fields for card 3DS (#3203) --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/router/src/configs/defaults.rs | 40 ++++++++++++++++++- .../router/src/connector/nmi/transformers.rs | 36 ++++++++++++++--- crates/router/src/services/api.rs | 8 ++++ 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 7b4380ba2db0..f9ae71d3b9ee 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -357,6 +357,7 @@ stripe = { payment_method = "bank_transfer" } nuvei = { payment_method = "card" } shift4 = { payment_method = "card" } bluesnap = { payment_method = "card" } +nmi = {payment_method = "card"} [dummy_connector] enabled = true # Whether dummy connector is enabled or not diff --git a/config/development.toml b/config/development.toml index b6a0f9f99cd9..d365abc46744 100644 --- a/config/development.toml +++ b/config/development.toml @@ -434,6 +434,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +nmi = {payment_method = "card"} [connector_customer] connector_list = "gocardless,stax,stripe" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index eab1ea5408c0..63be7339c7bc 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -247,6 +247,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +nmi = {payment_method = "card"} [dummy_connector] enabled = true diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 83a34b87dd0b..c90af883d6e3 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -1450,7 +1450,25 @@ impl Default for super::settings::RequiredFields { field_type: enums::FieldType::UserCardCvc, value: None, } - ) + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), ] ), common: HashMap::new(), @@ -3481,7 +3499,25 @@ impl Default for super::settings::RequiredFields { field_type: enums::FieldType::UserCardCvc, value: None, } - ) + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), ] ), common: HashMap::new(), diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 6146f4a45992..931dac5a9664 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -6,7 +6,10 @@ use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData}, + connector::utils::{ + self, AddressDetailsData, PaymentsAuthorizeRequestData, + PaymentsCompleteAuthorizeRequestData, RouterData, + }, core::errors, services, types::{self, api, storage::enums, transformers::ForeignFrom, ConnectorAuthType}, @@ -83,6 +86,9 @@ pub struct NmiVaultRequest { security_key: Secret, ccnumber: CardNumber, ccexp: Secret, + cvv: Secret, + first_name: Secret, + last_name: Secret, customer_vault: CustomerAction, } @@ -97,12 +103,16 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for NmiVaultRequest { type Error = Error; fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?; - let (ccnumber, ccexp) = get_card_details(item.request.payment_method_data.clone())?; + let (ccnumber, ccexp, cvv) = get_card_details(item.request.payment_method_data.clone())?; + let billing_details = item.get_billing_address()?; Ok(Self { security_key: auth_type.api_key, ccnumber, ccexp, + cvv, + first_name: billing_details.get_first_name()?.to_owned(), + last_name: billing_details.get_last_name()?.to_owned(), customer_vault: CustomerAction::AddCustomer, }) } @@ -110,7 +120,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for NmiVaultRequest { fn get_card_details( payment_method_data: Option, -) -> CustomResult<(CardNumber, Secret), errors::ConnectorError> { +) -> CustomResult<(CardNumber, Secret, Secret), errors::ConnectorError> { match payment_method_data { Some(api::PaymentMethodData::Card(ref card_details)) => Ok(( card_details.card_number.clone(), @@ -118,6 +128,7 @@ fn get_card_details( card_details, "".to_string(), ), + card_details.card_cvc.clone(), )), _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Nmi"), @@ -189,6 +200,7 @@ impl config: "public_key", }, )?, + order_id: item.data.connector_request_reference_id.clone(), }), mandate_reference: None, connector_metadata: None, @@ -220,13 +232,17 @@ impl #[derive(Debug, Serialize)] pub struct NmiCompleteRequest { + amount: f64, #[serde(rename = "type")] transaction_type: TransactionType, security_key: Secret, + orderid: String, + ccnumber: CardNumber, + ccexp: Secret, cardholder_auth: CardHolderAuthType, cavv: String, xid: String, - three_ds_version: ThreeDsVersion, + three_ds_version: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -240,6 +256,8 @@ pub enum CardHolderAuthType { pub enum ThreeDsVersion { #[serde(rename = "2.0.0")] VersionTwo, + #[serde(rename = "2.1.0")] + VersionTwoPointOne, #[serde(rename = "2.2.0")] VersionTwoPointTwo, } @@ -250,7 +268,8 @@ pub struct NmiRedirectResponseData { cavv: String, xid: String, card_holder_auth: CardHolderAuthType, - three_ds_version: ThreeDsVersion, + three_ds_version: Option, + order_id: String, } impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for NmiCompleteRequest { @@ -275,9 +294,16 @@ impl TryFrom<&NmiRouterData<&types::PaymentsCompleteAuthorizeRouterData>> for Nm field_name: "three_ds_data", })?; + let (ccnumber, ccexp, ..) = + get_card_details(item.router_data.request.payment_method_data.clone())?; + Ok(Self { + amount: item.amount, transaction_type, security_key: auth_type.api_key, + orderid: three_ds_data.order_id, + ccnumber, + ccexp, cardholder_auth: three_ds_data.card_holder_auth, cavv: three_ds_data.cavv, xid: three_ds_data.xid, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index a9fa574800ea..92fda578727c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -778,6 +778,7 @@ pub enum RedirectForm { currency: Currency, public_key: Secret, customer_vault_id: String, + order_id: String, }, } @@ -1507,6 +1508,7 @@ pub fn build_redirection_form( currency, public_key, customer_vault_id, + order_id, } => { let public_key_val = public_key.peek(); maud::html! { @@ -1563,6 +1565,12 @@ pub fn build_redirection_form( item4.value=e.threeDsVersion; responseForm.appendChild(item4); + var item5=document.createElement('input'); + item4.type='hidden'; + item4.name='orderId'; + item4.value='{order_id}'; + responseForm.appendChild(item5); + document.body.appendChild(responseForm); responseForm.submit(); }}); From a51c54d39d3687c6a06176895435ac66fa194d7b Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Tue, 26 Dec 2023 17:36:57 +0530 Subject: [PATCH 255/443] fix(connector): [Volt] Error handling for auth response (#3187) --- crates/router/src/connector/volt.rs | 15 ++++++++++++++- crates/router/src/connector/volt/transformers.rs | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index f239f5980145..bf36a7bff61e 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -223,7 +223,20 @@ impl ConnectorIntegration CustomResult { - self.build_error_response(res) + // auth error have different structure than common error + let response: volt::VoltAuthErrorResponse = res + .response + .parse_struct("VoltAuthErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code.to_string(), + message: response.message.clone(), + reason: Some(response.message), + attempt_status: None, + connector_transaction_id: None, + }) } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index cea56feb7145..9ee2a3f012eb 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -410,6 +410,12 @@ pub struct VoltErrorResponse { pub exception: VoltErrorException, } +#[derive(Debug, Deserialize)] +pub struct VoltAuthErrorResponse { + pub code: u64, + pub message: String, +} + #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct VoltErrorException { From aefe6184ec3e3156877c72988ca0f92454a47e7d Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 26 Dec 2023 17:37:27 +0530 Subject: [PATCH 256/443] feat(customers): Add JWT Authentication for `/customers` APIs (#3179) --- crates/api_models/src/user_role.rs | 3 ++ crates/router/src/routes/customers.rs | 39 +++++++++++++++---- .../router/src/services/authorization/info.rs | 10 +++++ .../src/services/authorization/permissions.rs | 4 ++ .../authorization/predefined_permissions.rs | 12 ++++++ crates/router/src/types/domain/user.rs | 1 + crates/router/src/utils/user_role.rs | 2 + 7 files changed, 63 insertions(+), 8 deletions(-) diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 735cd240b6e7..72fca2b2f084 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -32,6 +32,8 @@ pub enum Permission { DisputeWrite, MandateRead, MandateWrite, + CustomerRead, + CustomerWrite, FileRead, FileWrite, Analytics, @@ -53,6 +55,7 @@ pub enum PermissionModule { Routing, Analytics, Mandates, + Customer, Disputes, Files, ThreeDsDecisionManager, diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index cfc37cbdbb2a..2592d8837d50 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -4,7 +4,7 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, customers::*}, - services::{api, authentication as auth}, + services::{api, authentication as auth, authorization::permissions::Permission}, types::api::customers, }; @@ -36,7 +36,11 @@ pub async fn customers_create( &req, json_payload.into_inner(), |state, auth, req| create_customer(state, auth.merchant_account, auth.key_store, req), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -68,11 +72,14 @@ pub async fn customers_retrieve( }) .into_inner(); - let auth = + let auth = if auth::is_jwt_auth(req.headers()) { + Box::new(auth::JWTAuth(Permission::CustomerRead)) + } else { match auth::is_ephemeral_auth(req.headers(), &*state.store, &payload.customer_id).await { Ok(auth) => auth, Err(err) => return api::log_and_return_error_response(err), - }; + } + }; api::server_wrap( flow, @@ -110,7 +117,11 @@ pub async fn customers_list(state: web::Data, req: HttpRequest) -> Htt &req, (), |state, auth, _| list_customers(state, auth.merchant_account.merchant_id, auth.key_store), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -148,7 +159,11 @@ pub async fn customers_update( &req, json_payload.into_inner(), |state, auth, req| update_customer(state, auth.merchant_account, req, auth.key_store), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -185,7 +200,11 @@ pub async fn customers_delete( &req, payload, |state, auth, req| delete_customer(state, auth.merchant_account, req, auth.key_store), - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::CustomerWrite), + req.headers(), + ), api_locking::LockAction::NotApplicable, )) .await @@ -209,7 +228,11 @@ pub async fn get_customer_mandates( |state, auth, req| { crate::core::mandate::get_customer_mandates(state, auth.merchant_account, req) }, - &auth::ApiKeyAuth, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MandateRead), + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index c6b649f3de5c..cef93f82739d 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -38,6 +38,7 @@ pub enum PermissionModule { Routing, Analytics, Mandates, + Customer, Disputes, Files, ThreeDsDecisionManager, @@ -55,6 +56,7 @@ impl PermissionModule { Self::Forex => "Forex module permissions allow the user to view and query the forex rates", Self::Analytics => "Permission to view and analyse the data relating to payments, refunds, sdk etc.", Self::Mandates => "Everything related to mandates - like creating and viewing mandate related information are within this module", + Self::Customer => "Everything related to customers - like creating and viewing customer related information are within this module", Self::Disputes => "Everything related to disputes - like creating and viewing dispute related information are within this module", Self::Files => "Permissions for uploading, deleting and viewing files for disputes", Self::ThreeDsDecisionManager => "View and configure 3DS decision rules configured for a merchant", @@ -133,6 +135,14 @@ impl ModuleInfo { Permission::MandateWrite, ]), }, + PermissionModule::Customer => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[ + Permission::CustomerRead, + Permission::CustomerWrite, + ]), + }, PermissionModule::Disputes => Self { module: module_name, description, diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 708da97e1e39..426b048e88b7 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -19,6 +19,8 @@ pub enum Permission { DisputeWrite, MandateRead, MandateWrite, + CustomerRead, + CustomerWrite, FileRead, FileWrite, Analytics, @@ -55,6 +57,8 @@ impl Permission { Self::DisputeWrite => Some("Create and update disputes"), Self::MandateRead => Some("View mandates"), Self::MandateWrite => Some("Create and update mandates"), + Self::CustomerRead => Some("View customers"), + Self::CustomerWrite => Some("Create, update and delete customers"), Self::FileRead => Some("View files"), Self::FileWrite => Some("Create, update and delete files"), Self::Analytics => Some("Access to analytics module"), diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs index a9f2b864d0ad..c489f1fc9638 100644 --- a/crates/router/src/services/authorization/predefined_permissions.rs +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -52,6 +52,8 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::DisputeWrite, Permission::MandateRead, Permission::MandateWrite, + Permission::CustomerRead, + Permission::CustomerWrite, Permission::FileRead, Permission::FileWrite, Permission::Analytics, @@ -79,6 +81,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::Analytics, Permission::DisputeRead, Permission::MandateRead, + Permission::CustomerRead, Permission::FileRead, Permission::UsersRead, ], @@ -112,6 +115,8 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::DisputeWrite, Permission::MandateRead, Permission::MandateWrite, + Permission::CustomerRead, + Permission::CustomerWrite, Permission::FileRead, Permission::FileWrite, Permission::Analytics, @@ -150,6 +155,8 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::DisputeWrite, Permission::MandateRead, Permission::MandateWrite, + Permission::CustomerRead, + Permission::CustomerWrite, Permission::FileRead, Permission::FileWrite, Permission::Analytics, @@ -175,6 +182,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::SurchargeDecisionManagerRead, Permission::DisputeRead, Permission::MandateRead, + Permission::CustomerRead, Permission::FileRead, Permission::Analytics, Permission::UsersRead, @@ -198,6 +206,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::SurchargeDecisionManagerRead, Permission::DisputeRead, Permission::MandateRead, + Permission::CustomerRead, Permission::FileRead, Permission::Analytics, Permission::UsersRead, @@ -223,6 +232,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::SurchargeDecisionManagerRead, Permission::DisputeRead, Permission::MandateRead, + Permission::CustomerRead, Permission::FileRead, Permission::Analytics, Permission::UsersRead, @@ -252,6 +262,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::SurchargeDecisionManagerWrite, Permission::DisputeRead, Permission::MandateRead, + Permission::CustomerRead, Permission::FileRead, Permission::Analytics, Permission::UsersRead, @@ -273,6 +284,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: Permission::MerchantAccountRead, Permission::MerchantConnectorAccountRead, Permission::MandateRead, + Permission::CustomerRead, Permission::FileRead, Permission::FileWrite, Permission::Analytics, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index a595afa4a27c..ce8350bd9eb0 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -788,6 +788,7 @@ impl From for user_role_api::PermissionModule { info::PermissionModule::Routing => Self::Routing, info::PermissionModule::Analytics => Self::Analytics, info::PermissionModule::Mandates => Self::Mandates, + info::PermissionModule::Customer => Self::Customer, info::PermissionModule::Disputes => Self::Disputes, info::PermissionModule::Files => Self::Files, info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager, diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index 0026984fdb9a..4449338402fb 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -74,6 +74,8 @@ impl TryFrom<&Permission> for user_role_api::Permission { Permission::DisputeWrite => Ok(Self::DisputeWrite), Permission::MandateRead => Ok(Self::MandateRead), Permission::MandateWrite => Ok(Self::MandateWrite), + Permission::CustomerRead => Ok(Self::CustomerRead), + Permission::CustomerWrite => Ok(Self::CustomerWrite), Permission::FileRead => Ok(Self::FileRead), Permission::FileWrite => Ok(Self::FileWrite), Permission::Analytics => Ok(Self::Analytics), From 561bb107396a036ac08ebc8ce9453956c47afe68 Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:29:00 +0530 Subject: [PATCH 257/443] ci(postman): Added delay in checkout collection (#3206) Co-authored-by: Likhin Bopanna --- .../Payments - Retrieve/event.prerequest.js | 3 +++ .../3DS Payment/Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve-copy/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve-copy/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve-copy/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve-copy/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../QuickStart/Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ .../Payments - Retrieve/event.prerequest.js | 3 +++ 25 files changed, 75 insertions(+) create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve-copy/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/event.prerequest.js create mode 100644 postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/event.prerequest.js diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/3DS Payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Cancel After Partial Capture/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Retrieve After Partial Capture/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js index e69de29bb2d1..97b68c987bdf 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario10-Multiple Captures/Successful Partial Capture and Refund/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario11-Save card flow/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario12-Don't Pass CVV for save card flow and verify success payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario13-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario14-Save card payment with manual capture/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario2-Create payment with confirm false/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario3-Create payment without PMD/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario4-Create payment with Manual capture with confirm false/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario5-Create payment with Manual capture/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario6-Create Partial Capture payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario7-Void the payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario8-Refund full payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve-copy/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Happy Cases/Scenario9-Partial refund/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario3-Capture greater amount/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund exceeds amount/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/event.prerequest.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/event.prerequest.js new file mode 100644 index 000000000000..97b68c987bdf --- /dev/null +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario6-Refund for unsuccessful payment/Payments - Retrieve/event.prerequest.js @@ -0,0 +1,3 @@ +setTimeout(function(){ + console.log("Sleeping for 3 seconds before next request."); +}, 3000); \ No newline at end of file From 18eca7e9fbe6cdc101bd135c4618882b7a5455bf Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:34:51 +0530 Subject: [PATCH 258/443] feat(connector): [BOA] Populate merchant_defined_information with metadata (#3208) --- .../connector/bankofamerica/transformers.rs | 58 ++++++++++++++++--- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/types.rs | 2 + .../router/src/types/api/verify_connector.rs | 1 + crates/router/tests/connectors/aci.rs | 1 + crates/router/tests/connectors/adyen.rs | 1 + crates/router/tests/connectors/bitpay.rs | 1 + crates/router/tests/connectors/cashtocode.rs | 1 + crates/router/tests/connectors/coinbase.rs | 1 + crates/router/tests/connectors/cryptopay.rs | 1 + crates/router/tests/connectors/opennode.rs | 1 + crates/router/tests/connectors/utils.rs | 1 + crates/router/tests/connectors/worldline.rs | 1 + 13 files changed, 64 insertions(+), 7 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 477bbec7350b..def93ec5f83f 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use api_models::payments; use base64::Engine; use common_utils::pii; use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ connector::utils::{ @@ -83,6 +86,8 @@ pub struct BankOfAmericaPaymentsRequest { payment_information: PaymentInformation, order_information: OrderInformationWithBill, client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } #[derive(Debug, Serialize)] @@ -92,6 +97,13 @@ pub struct ProcessingInformation { payment_solution: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantDefinedInformation { + key: u8, + value: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { @@ -309,6 +321,23 @@ impl From<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> } } +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: HashMap = + serde_json::from_str(&metadata.to_string()).unwrap_or(HashMap::new()); + let mut vector: Self = Self::new(); + let mut iter = 1; + for (key, value) in hashmap { + vector.push(MerchantDefinedInformation { + key: iter, + value: format!("{key}={value}"), + }); + iter += 1; + } + vector + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { @@ -331,13 +360,11 @@ impl let email = item.router_data.request.get_email()?; let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; let order_information = OrderInformationWithBill::from((item, bill_to)); - let card_issuer = ccard.get_card_issuer(); let card_type = match card_issuer { Ok(issuer) => Some(String::from(issuer)), Err(_) => None, }; - let payment_information = PaymentInformation::Cards(CardPaymentInformation { card: Card { number: ccard.card_number, @@ -347,15 +374,19 @@ impl card_type, }, }); - let processing_information = ProcessingInformation::from((item, None)); let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information, payment_information, order_information, client_reference_information, + merchant_defined_information, }) } } @@ -379,10 +410,8 @@ impl let processing_information = ProcessingInformation::from((item, Some(PaymentSolution::ApplePay))); let client_reference_information = ClientReferenceInformation::from(item); - let expiration_month = apple_pay_data.get_expiry_month()?; let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; - let payment_information = PaymentInformation::ApplePay(ApplePayPaymentInformation { tokenized_card: TokenizedCard { number: apple_pay_data.application_primary_account_number, @@ -392,12 +421,17 @@ impl expiration_month, }, }); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information, payment_information, order_information, client_reference_information, + merchant_defined_information, }) } } @@ -418,7 +452,6 @@ impl let email = item.router_data.request.get_email()?; let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; let order_information = OrderInformationWithBill::from((item, bill_to)); - let payment_information = PaymentInformation::GooglePay(GooglePayPaymentInformation { fluid_data: FluidData { value: Secret::from( @@ -426,16 +459,20 @@ impl ), }, }); - let processing_information = ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information, payment_information, order_information, client_reference_information, + merchant_defined_information, }) } } @@ -480,10 +517,17 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> }, }, ); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from( + metadata.peek().to_owned(), + ) + }); Ok(Self { processing_information, payment_information, order_information, + merchant_defined_information, client_reference_information, }) } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f0d8c9fd7552..577480d311d7 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1076,6 +1076,7 @@ impl TryFrom> for types::PaymentsAuthoriz Some(RequestIncrementalAuthorization::True) | Some(RequestIncrementalAuthorization::Default) ), + metadata: additional_data.payment_data.payment_intent.metadata, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index cc14fe36a044..50aba3de7b53 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -407,6 +407,7 @@ pub struct PaymentsAuthorizeData { pub surcharge_details: Option, pub customer_id: Option, pub request_incremental_authorization: bool, + pub metadata: Option, } #[derive(Debug, Clone, Default)] @@ -1245,6 +1246,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { customer_id: None, surcharge_details: None, request_incremental_authorization: data.request.request_incremental_authorization, + metadata: None, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 04a9d76632b0..c5fcce8b185e 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -27,6 +27,7 @@ impl VerifyConnectorData { amount: 1000, confirm: true, currency: storage_enums::Currency::USD, + metadata: None, mandate_id: None, webhook_url: None, customer_id: None, diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index c46cca8e4dd7..35c9cbd952d3 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -70,6 +70,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 14177e6fb500..97dca3baa52b 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -158,6 +158,7 @@ impl AdyenTest { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 3c9f08bf1b69..8bac7c13c85f 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -93,6 +93,7 @@ fn payment_method_details() -> Option { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index a7c95936fbe8..68c4eb94bf32 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -68,6 +68,7 @@ impl CashtocodeTest { customer_id: Some("John Doe".to_owned()), surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 2ddb5464d4df..73ee93178c01 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -95,6 +95,7 @@ fn payment_method_details() -> Option { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index 11e556215c35..5df8d80461fa 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -93,6 +93,7 @@ fn payment_method_details() -> Option { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 707192e01c3b..b140a7c05170 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -94,6 +94,7 @@ fn payment_method_details() -> Option { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1c384b2e5137..62fce84f1f9d 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -911,6 +911,7 @@ impl Default for PaymentAuthorizeType { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }; Self(data) } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 6a92e0dc93f6..4f7a94780a59 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -103,6 +103,7 @@ impl WorldlineTest { customer_id: None, surcharge_details: None, request_incremental_authorization: false, + metadata: None, }) } } From e06ba148b666772fe79d7050d0c505dd2f04f87c Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 29 Dec 2023 14:23:31 +0530 Subject: [PATCH 259/443] feat(connector): [CYBERSOURCE] Refactor cybersource (#3215) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/cybersource.rs | 94 ++++++++- .../src/connector/cybersource/transformers.rs | 192 ++++++++++++++++-- 2 files changed, 267 insertions(+), 19 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 1347fbfc93a2..3496f2483ab1 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -446,6 +446,27 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -606,6 +627,33 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } } impl ConnectorIntegration @@ -626,9 +674,8 @@ impl ConnectorIntegration CustomResult { let connector_payment_id = req.request.connector_transaction_id.clone(); Ok(format!( - "{}pts/v2/payments/{}/voids", - self.base_url(connectors), - connector_payment_id + "{}pts/v2/payments/{connector_payment_id}/reversals", + self.base_url(connectors) )) } @@ -638,10 +685,26 @@ impl ConnectorIntegration CustomResult { - Ok(RequestContent::Json(Box::new(serde_json::json!({})))) + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Amount", + })?, + req, + ))?; + let connector_req = cybersource::CybersourceVoidRequest::try_from(&connector_router_data)?; + + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -682,6 +745,27 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status: None, + connector_transaction_id: None, + }) + } } impl api::Refund for Cybersource {} diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 1d828a95d60a..147e50a9e918 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use api_models::payments; use base64::Engine; use common_utils::pii; use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ connector::utils::{ @@ -134,6 +137,8 @@ pub struct CybersourcePaymentsRequest { payment_information: PaymentInformation, order_information: OrderInformationWithBill, client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } #[derive(Debug, Serialize)] @@ -148,6 +153,13 @@ pub struct ProcessingInformation { payment_solution: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantDefinedInformation { + key: u8, + value: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CybersourceActionsList { @@ -218,6 +230,19 @@ pub struct TokenizedCard { transaction_type: TransactionType, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenizedCard { + transaction_type: TransactionType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayTokenPaymentInformation { + fluid_data: FluidData, + tokenized_card: ApplePayTokenizedCard, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApplePayPaymentInformation { @@ -242,6 +267,7 @@ pub enum PaymentInformation { Cards(CardPaymentInformation), GooglePay(GooglePayPaymentInformation), ApplePay(ApplePayPaymentInformation), + ApplePayToken(ApplePayTokenPaymentInformation), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -441,6 +467,23 @@ fn build_bill_to( }) } +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: HashMap = + serde_json::from_str(&metadata.to_string()).unwrap_or(HashMap::new()); + let mut vector: Self = Self::new(); + let mut iter = 1; + for (key, value) in hashmap { + vector.push(MerchantDefinedInformation { + key: iter, + value: format!("{key}={value}"), + }); + iter += 1; + } + vector + } +} + impl TryFrom<( &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, @@ -491,15 +534,19 @@ impl card, instrument_identifier, }); - let processing_information = ProcessingInformation::from((item, None)); let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information, payment_information, order_information, client_reference_information, + merchant_defined_information, }) } } @@ -525,7 +572,6 @@ impl let client_reference_information = ClientReferenceInformation::from(item); let expiration_month = apple_pay_data.get_expiry_month()?; let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; - let payment_information = PaymentInformation::ApplePay(ApplePayPaymentInformation { tokenized_card: TokenizedCard { number: apple_pay_data.application_primary_account_number, @@ -535,12 +581,17 @@ impl expiration_month, }, }); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information, payment_information, order_information, client_reference_information, + merchant_defined_information, }) } } @@ -569,16 +620,20 @@ impl ), }, }); - let processing_information = ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information, payment_information, order_information, client_reference_information, + merchant_defined_information, }) } } @@ -593,14 +648,50 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> match item.router_data.request.payment_method_data.clone() { payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - payments::WalletData::ApplePay(_) => { - let payment_method_token = item.router_data.get_payment_method_token()?; - match payment_method_token { - types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { - Self::try_from((item, decrypt_data)) - } - types::PaymentMethodToken::Token(_) => { - Err(errors::ConnectorError::InvalidWalletToken)? + payments::WalletData::ApplePay(apple_pay_data) => { + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, + None => { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + let processing_information = ProcessingInformation::from(( + item, + Some(PaymentSolution::ApplePay), + )); + let client_reference_information = + ClientReferenceInformation::from(item); + let payment_information = PaymentInformation::ApplePayToken( + ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }, + ); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from( + metadata.peek().to_owned(), + ) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + }) } } } @@ -737,6 +828,51 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout } } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceVoidRequest { + client_reference_information: ClientReferenceInformation, + reversal_information: ReversalInformation, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReversalInformation { + amount_details: Amount, + reason: String, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCancelRouterData>> for CybersourceVoidRequest { + type Error = error_stack::Report; + fn try_from( + value: &CybersourceRouterData<&types::PaymentsCancelRouterData>, + ) -> Result { + Ok(Self { + client_reference_information: ClientReferenceInformation { + code: Some(value.router_data.connector_request_reference_id.clone()), + }, + reversal_information: ReversalInformation { + amount_details: Amount { + total_amount: value.amount.to_owned(), + currency: value.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "Currency", + }, + )?, + }, + reason: value + .router_data + .request + .cancellation_reason + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "Cancellation Reason", + })?, + }, + }) + } +} + pub struct CybersourceAuthType { pub(super) api_key: Secret, pub(super) merchant_account: Secret, @@ -1079,9 +1215,21 @@ impl ..item.data }) } - CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { - Ok(Self::from((&error_response.clone(), item))) - } + CybersourcePaymentsResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: error_response.error_information.reason, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }), + status: enums::AttemptStatus::Failure, + ..item.data + }), } } } @@ -1496,6 +1644,22 @@ pub struct CybersourceStandardErrorResponse { pub details: Option>, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceServerErrorResponse { + pub status: Option, + pub message: Option, + pub reason: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Reason { + SystemError, + ServerTimeout, + ServiceTimeout, +} + #[derive(Debug, Deserialize)] pub struct CybersourceAuthenticationErrorResponse { pub response: AuthenticationErrorInformation, From 0f72b5527aab221b8e69e737e5d19abdd0696150 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Wed, 3 Jan 2024 14:53:10 +0530 Subject: [PATCH 260/443] fix(middleware): add support for logging request-id sent in request (#3225) --- crates/router/src/middleware.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/router/src/middleware.rs b/crates/router/src/middleware.rs index 1576432e26ed..0f2c5bd2cb7f 100644 --- a/crates/router/src/middleware.rs +++ b/crates/router/src/middleware.rs @@ -43,16 +43,21 @@ where actix_web::dev::forward_ready!(service); fn call(&self, req: actix_web::dev::ServiceRequest) -> Self::Future { + let old_x_request_id = req.headers().get("x-request-id").cloned(); let mut req = req; let request_id_fut = req.extract::(); let response_fut = self.service.call(req); Box::pin(async move { let request_id = request_id_fut.await?; + let request_id = request_id.as_hyphenated().to_string(); + if let Some(upstream_request_id) = old_x_request_id { + router_env::logger::info!(?request_id, ?upstream_request_id); + } let mut response = response_fut.await?; response.headers_mut().append( http::header::HeaderName::from_static("x-request-id"), - http::HeaderValue::from_str(&request_id.as_hyphenated().to_string())?, + http::HeaderValue::from_str(&request_id)?, ); Ok(response) From 46e84a6b0cb74bccc3a63d9568ca2668866a960f Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Wed, 3 Jan 2024 14:57:18 +0530 Subject: [PATCH 261/443] ci(postman): Added necessary card networks (#3230) Co-authored-by: Likhin Bopanna --- .../Payment Connector - Create/request.json | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 9500716c12c9..7ca3d82e793e 100644 --- a/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/checkout/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -56,10 +56,12 @@ "payment_method_types": [ { "payment_method_type": "credit", - "card_networks": [ - "Visa", - "Mastercard" - ], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, @@ -67,10 +69,12 @@ }, { "payment_method_type": "debit", - "card_networks": [ - "Visa", - "Mastercard" - ], + "card_networks": ["AmericanExpress", + "Discover", + "Interac", + "JCB", + "Mastercard", + "Visa", "DinersClub","UnionPay","RuPay"], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, From 51e1fac556fdd8775e0bbc858b0b3cc50a7e88ec Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Wed, 3 Jan 2024 17:42:05 +0530 Subject: [PATCH 262/443] chore: fix channel handling for consumer workflow loop (#3223) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/scheduler/src/consumer.rs | 25 +++++++++++++++---------- crates/scheduler/src/utils.rs | 4 ++-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/scheduler/src/consumer.rs b/crates/scheduler/src/consumer.rs index 08899552704a..ef0386bec299 100644 --- a/crates/scheduler/src/consumer.rs +++ b/crates/scheduler/src/consumer.rs @@ -61,7 +61,7 @@ pub async fn start_consumer( let handle = signal.handle(); let task_handle = tokio::spawn(common_utils::signals::signal_handler(signal, tx)); - loop { + 'consumer: loop { match rx.try_recv() { Err(mpsc::error::TryRecvError::Empty) => { interval.tick().await; @@ -71,7 +71,7 @@ pub async fn start_consumer( continue; } - tokio::task::spawn(pt_utils::consumer_operation_handler( + pt_utils::consumer_operation_handler( state.clone(), settings.clone(), |err| { @@ -79,19 +79,23 @@ pub async fn start_consumer( }, sync::Arc::clone(&consumer_operation_counter), workflow_selector, - )); + ) + .await; } Ok(()) | Err(mpsc::error::TryRecvError::Disconnected) => { logger::debug!("Awaiting shutdown!"); rx.close(); - shutdown_interval.tick().await; - let active_tasks = consumer_operation_counter.load(atomic::Ordering::Acquire); - match active_tasks { - 0 => { - logger::info!("Terminating consumer"); - break; + loop { + shutdown_interval.tick().await; + let active_tasks = consumer_operation_counter.load(atomic::Ordering::Acquire); + logger::error!("{}", active_tasks); + match active_tasks { + 0 => { + logger::info!("Terminating consumer"); + break 'consumer; + } + _ => continue, } - _ => continue, } } } @@ -204,6 +208,7 @@ where T: SchedulerAppState, { tracing::Span::current().record("workflow_id", Uuid::new_v4().to_string()); + logger::info!("{:?}", process.name.as_ref()); let res = workflow_selector .trigger_workflow(&state.clone(), process.clone()) .await; diff --git a/crates/scheduler/src/utils.rs b/crates/scheduler/src/utils.rs index 53f14bd1fb9c..32fd97fca334 100644 --- a/crates/scheduler/src/utils.rs +++ b/crates/scheduler/src/utils.rs @@ -252,7 +252,7 @@ pub async fn consumer_operation_handler( E: FnOnce(error_stack::Report), T: SchedulerAppState, { - consumer_operation_counter.fetch_add(1, atomic::Ordering::Release); + consumer_operation_counter.fetch_add(1, atomic::Ordering::SeqCst); let start_time = std_time::Instant::now(); match consumer::consumer_operations(&state, &settings, workflow_selector).await { @@ -263,7 +263,7 @@ pub async fn consumer_operation_handler( let duration = end_time.saturating_duration_since(start_time).as_secs_f64(); logger::debug!("Time taken to execute consumer_operation: {}s", duration); - let current_count = consumer_operation_counter.fetch_sub(1, atomic::Ordering::Release); + let current_count = consumer_operation_counter.fetch_sub(1, atomic::Ordering::SeqCst); logger::info!("Current tasks being executed: {}", current_count); } From 6a1743ebe993d5abb53f2ce1b8b383aa4a9553fb Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Wed, 3 Jan 2024 18:58:10 +0530 Subject: [PATCH 263/443] fix(core): fix recurring mandates flow for cyber source (#3224) Co-authored-by: Samraat Bansal --- config/development.toml | 4 +- .../src/connector/cybersource/transformers.rs | 326 ++++++++++++------ crates/router/src/core/payments.rs | 24 +- 3 files changed, 219 insertions(+), 135 deletions(-) diff --git a/config/development.toml b/config/development.toml index d365abc46744..6e7e040906a5 100644 --- a/config/development.toml +++ b/config/development.toml @@ -468,8 +468,8 @@ connectors_with_webhook_source_verification_call = "paypal" [mandates.supported_payment_methods] pay_later.klarna = { connector_list = "adyen" } -wallet.google_pay = { connector_list = "stripe,adyen" } -wallet.apple_pay = { connector_list = "stripe,adyen" } +wallet.google_pay = { connector_list = "stripe,adyen,cybersource" } +wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon" } wallet.paypal = { connector_list = "adyen" } card.credit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 147e50a9e918..a5a0a7237ef5 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -78,7 +78,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { }; let (action_list, action_token_types, authorization_options) = ( Some(vec![CybersourceActionsList::TokenCreate]), - Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(vec![CybersourceActionsTokenType::PaymentInstrument]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), @@ -89,38 +89,122 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { }), ); - let processing_information = ProcessingInformation { - capture: Some(false), - capture_options: None, - action_list, - action_token_types, - authorization_options, - commerce_indicator: CybersourceCommerceIndicator::Internet, - payment_solution: None, - }; - let client_reference_information = ClientReferenceInformation { code: Some(item.connector_request_reference_id.clone()), }; - let payment_information = match item.request.payment_method_data.clone() { + let (payment_information, solution) = match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(ccard) => { - let card = CardDetails::PaymentCard(Card { - number: ccard.card_number, - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - security_code: ccard.card_cvc, - card_type: None, - }); - PaymentInformation::Cards(CardPaymentInformation { - card, - instrument_identifier: None, - }) + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + ( + PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }), + None, + ) } + + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::ApplePay(apple_pay_data) => { + match item.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + let expiration_month = decrypt_data.get_expiry_month()?; + let expiration_year = decrypt_data.get_four_digit_expiry_year()?; + ( + PaymentInformation::ApplePay(ApplePayPaymentInformation { + tokenized_card: TokenizedCard { + number: decrypt_data.application_primary_account_number, + cryptogram: decrypt_data + .payment_data + .online_payment_cryptogram, + transaction_type: TransactionType::ApplePay, + expiration_year, + expiration_month, + }, + }), + Some(PaymentSolution::ApplePay), + ) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } + }, + None => ( + PaymentInformation::ApplePayToken(ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }), + Some(PaymentSolution::ApplePay), + ), + } + } + payments::WalletData::GooglePay(google_pay_data) => ( + PaymentInformation::GooglePay(GooglePayPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE + .encode(google_pay_data.tokenization_data.token), + ), + }, + }), + Some(PaymentSolution::GooglePay), + ), + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))?, + }, _ => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), ))?, }; + + let processing_information = ProcessingInformation { + capture: Some(false), + capture_options: None, + action_list, + action_token_types, + authorization_options, + commerce_indicator: CybersourceCommerceIndicator::Internet, + payment_solution: solution.map(String::from), + }; Ok(Self { processing_information, payment_information, @@ -169,7 +253,7 @@ pub enum CybersourceActionsList { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub enum CybersourceActionsTokenType { - InstrumentIdentifier, + PaymentInstrument, } #[derive(Debug, Serialize)] @@ -216,8 +300,7 @@ pub struct CaptureOptions { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CardPaymentInformation { - card: CardDetails, - instrument_identifier: Option, + card: Card, } #[derive(Debug, Serialize)] @@ -249,6 +332,12 @@ pub struct ApplePayPaymentInformation { tokenized_card: TokenizedCard, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MandatePaymentInformation { + payment_instrument: Option, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct FluidData { @@ -268,20 +357,13 @@ pub enum PaymentInformation { GooglePay(GooglePayPaymentInformation), ApplePay(ApplePayPaymentInformation), ApplePayToken(ApplePayTokenPaymentInformation), + MandatePayment(MandatePaymentInformation), } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CybersoucreInstrumentIdentifier { +pub struct CybersoucrePaymentInstrument { id: String, } - -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum CardDetails { - PaymentCard(Card), - MandateCard(MandateCardDetails), -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Card { @@ -292,14 +374,6 @@ pub struct Card { #[serde(rename = "type")] card_type: Option, } - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MandateCardDetails { - expiration_month: Secret, - expiration_year: Secret, -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformationWithBill { @@ -394,7 +468,7 @@ impl if item.router_data.request.setup_future_usage.is_some() { ( Some(vec![CybersourceActionsList::TokenCreate]), - Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), + Some(vec![CybersourceActionsTokenType::PaymentInstrument]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), @@ -507,33 +581,16 @@ impl Err(_) => None, }; - let instrument_identifier = - item.router_data - .request - .connector_mandate_id() - .map(|mandate_token_id| CybersoucreInstrumentIdentifier { - id: mandate_token_id, - }); - - let card = if instrument_identifier.is_some() { - CardDetails::MandateCard(MandateCardDetails { - expiration_month: ccard.card_exp_month, - expiration_year: ccard.card_exp_year, - }) - } else { - CardDetails::PaymentCard(Card { + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { number: ccard.card_number, expiration_month: ccard.card_exp_month, expiration_year: ccard.card_exp_year, security_code: ccard.card_cvc, card_type, - }) - }; - - let payment_information = PaymentInformation::Cards(CardPaymentInformation { - card, - instrument_identifier, + }, }); + let processing_information = ProcessingInformation::from((item, None)); let client_reference_information = ClientReferenceInformation::from(item); let merchant_defined_information = @@ -726,13 +783,42 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> ) .into()), }, + payments::PaymentMethodData::MandatePayment => { + let processing_information = ProcessingInformation::from((item, None)); + let payment_instrument = + item.router_data + .request + .connector_mandate_id() + .map(|mandate_token_id| CybersoucrePaymentInstrument { + id: mandate_token_id, + }); + + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + let payment_information = + PaymentInformation::MandatePayment(MandatePaymentInformation { + payment_instrument, + }); + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + }) + } payments::PaymentMethodData::CardRedirect(_) | payments::PaymentMethodData::PayLater(_) | payments::PaymentMethodData::BankRedirect(_) | payments::PaymentMethodData::BankDebit(_) | payments::PaymentMethodData::BankTransfer(_) | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::MandatePayment | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) @@ -1019,14 +1105,11 @@ pub struct CybersourcePaymentsIncrementalAuthorizationResponse { error_information: Option, } -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CybersourceSetupMandatesResponse { - id: String, - status: CybersourcePaymentStatus, - error_information: Option, - client_reference_information: Option, - token_information: Option, +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceSetupMandatesResponse { + ClientReferenceInformation(CybersourceClientReferenceResponse), + ErrorInformation(CybersourceErrorInformationResponse), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1038,7 +1121,7 @@ pub struct ClientReferenceInformation { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceTokenInformation { - instrument_identifier: CybersoucreInstrumentIdentifier, + payment_instrument: CybersoucrePaymentInstrument, } #[derive(Debug, Clone, Deserialize)] @@ -1161,7 +1244,7 @@ fn get_payment_response( .token_information .clone() .map(|token_info| types::MandateReference { - connector_mandate_id: Some(token_info.instrument_identifier.id), + connector_mandate_id: Some(token_info.payment_instrument.id), payment_method_id: None, }); Ok(types::PaymentsResponseData::TransactionResponse { @@ -1327,51 +1410,66 @@ impl types::PaymentsResponseData, >, ) -> Result { - let mandate_reference = - item.response - .token_information - .map(|token_info| types::MandateReference { - connector_mandate_id: Some(token_info.instrument_identifier.id), - payment_method_id: None, + match item.response { + CybersourceSetupMandatesResponse::ClientReferenceInformation(info_response) => { + let mandate_reference = info_response.token_information.clone().map(|token_info| { + types::MandateReference { + connector_mandate_id: Some(token_info.payment_instrument.id), + payment_method_id: None, + } }); - let mut mandate_status = enums::AttemptStatus::foreign_from((item.response.status, false)); - if matches!(mandate_status, enums::AttemptStatus::Authorized) { - //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. - mandate_status = enums::AttemptStatus::Charged - } - Ok(Self { - status: mandate_status, - response: match item.response.error_information { - Some(error) => Err(types::ErrorResponse { + let mut mandate_status = + enums::AttemptStatus::foreign_from((info_response.status.clone(), false)); + if matches!(mandate_status, enums::AttemptStatus::Authorized) { + //In case of zero auth mandates we want to make the payment reach the terminal status so we are converting the authorized status to charged as well. + mandate_status = enums::AttemptStatus::Charged + } + let error_response = + get_error_response_if_failure((&info_response, mandate_status, item.http_code)); + + Ok(Self { + status: mandate_status, + response: match error_response { + Some(error) => Err(error), + None => Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + info_response.id.clone(), + ), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .clone() + .unwrap_or(info_response.id), + ), + incremental_authorization_allowed: Some( + mandate_status == enums::AttemptStatus::Authorized, + ), + }), + }, + ..item.data + }) + } + CybersourceSetupMandatesResponse::ErrorInformation(error_response) => Ok(Self { + response: Err(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), - message: error + message: error_response + .error_information .message .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error.reason, + reason: error_response.error_information.reason, status_code: item.http_code, attempt_status: None, - connector_transaction_id: Some(item.response.id), - }), - _ => Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId( - item.response.id.clone(), - ), - redirection_data: None, - mandate_reference, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: item - .response - .client_reference_information - .map(|cref| cref.code) - .unwrap_or(Some(item.response.id)), - incremental_authorization_allowed: Some( - mandate_status == enums::AttemptStatus::Authorized, - ), + connector_transaction_id: Some(error_response.id.clone()), }), - }, - ..item.data - }) + status: enums::AttemptStatus::Failure, + ..item.data + }), + } } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index bfd747640d3f..aed22eaedc8f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1770,24 +1770,10 @@ where .unwrap_or(false); let payment_data_and_tokenization_action = match connector { - Some(connector_name) if is_mandate => { - if connector_name == *"cybersource" { - let (_operation, payment_method_data) = operation - .to_domain()? - .make_pm_data( - state, - payment_data, - validate_result.storage_scheme, - merchant_key_store, - ) - .await?; - payment_data.payment_method_data = payment_method_data; - } - ( - payment_data.to_owned(), - TokenizationAction::SkipConnectorTokenization, - ) - } + Some(_) if is_mandate => ( + payment_data.to_owned(), + TokenizationAction::SkipConnectorTokenization, + ), Some(connector) if is_operation_confirm(&operation) => { let payment_method = &payment_data .payment_attempt @@ -1870,7 +1856,7 @@ where }; (payment_data.to_owned(), connector_tokenization_action) } - Some(_) | None => ( + _ => ( payment_data.to_owned(), TokenizationAction::SkipConnectorTokenization, ), From 0248d35dd49d2dc7e5e4da6b60a3ee3577c8eac9 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 06:46:24 +0000 Subject: [PATCH 264/443] test(postman): update postman collection files --- .../checkout.postman_collection.json | 242 +++++++++++++++++- 1 file changed, 236 insertions(+), 6 deletions(-) diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index a46cae1df50e..d510b1c2a17f 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -424,7 +424,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"checkout\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"api_secret\":\"{{connector_api_secret}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":false,\"installment_payment_enabled\":false}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -559,6 +559,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -1001,6 +1012,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -1516,6 +1538,17 @@ { "name": "Payments - Retrieve-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -2336,6 +2369,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -2969,6 +3013,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -3234,6 +3289,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -3914,6 +3980,17 @@ { "name": "Payments - Retrieve-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -4222,6 +4299,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -4699,6 +4787,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -5191,6 +5290,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -5847,6 +5957,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -6324,6 +6445,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -6801,6 +6933,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -7230,6 +7373,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -7507,6 +7661,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -8029,6 +8194,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -8588,6 +8764,17 @@ { "name": "Payments - Retrieve-copy", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -9343,7 +9530,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -9816,7 +10005,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -10061,7 +10252,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -10305,7 +10498,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -11173,7 +11368,9 @@ "listen": "prerequest", "script": { "exec": [ - "" + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" ], "type": "text/javascript" } @@ -12357,6 +12554,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -13152,6 +13360,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -13547,6 +13766,17 @@ { "name": "Payments - Retrieve", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "setTimeout(function(){", + " console.log(\"Sleeping for 3 seconds before next request.\");", + "}, 3000);" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { From 72968dcecf16c8821a383a826e4e35408b035633 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 06:46:24 +0000 Subject: [PATCH 265/443] chore(version): v1.106.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd711b2e605..30b7957272c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.106.0 (2024-01-04) + +### Features + +- **connector:** + - [BOA] Populate merchant_defined_information with metadata ([#3208](https://github.com/juspay/hyperswitch/pull/3208)) ([`18eca7e`](https://github.com/juspay/hyperswitch/commit/18eca7e9fbe6cdc101bd135c4618882b7a5455bf)) + - [CYBERSOURCE] Refactor cybersource ([#3215](https://github.com/juspay/hyperswitch/pull/3215)) ([`e06ba14`](https://github.com/juspay/hyperswitch/commit/e06ba148b666772fe79d7050d0c505dd2f04f87c)) +- **customers:** Add JWT Authentication for `/customers` APIs ([#3179](https://github.com/juspay/hyperswitch/pull/3179)) ([`aefe618`](https://github.com/juspay/hyperswitch/commit/aefe6184ec3e3156877c72988ca0f92454a47e7d)) + +### Bug Fixes + +- **connector:** [Volt] Error handling for auth response ([#3187](https://github.com/juspay/hyperswitch/pull/3187)) ([`a51c54d`](https://github.com/juspay/hyperswitch/commit/a51c54d39d3687c6a06176895435ac66fa194d7b)) +- **core:** Fix recurring mandates flow for cyber source ([#3224](https://github.com/juspay/hyperswitch/pull/3224)) ([`6a1743e`](https://github.com/juspay/hyperswitch/commit/6a1743ebe993d5abb53f2ce1b8b383aa4a9553fb)) +- **middleware:** Add support for logging request-id sent in request ([#3225](https://github.com/juspay/hyperswitch/pull/3225)) ([`0f72b55`](https://github.com/juspay/hyperswitch/commit/0f72b5527aab221b8e69e737e5d19abdd0696150)) + +### Refactors + +- **connector:** [NMI] Include mandatory fields for card 3DS ([#3203](https://github.com/juspay/hyperswitch/pull/3203)) ([`a46b8a7`](https://github.com/juspay/hyperswitch/commit/a46b8a7b05367fbbdbf4fca89d8a6b29110a4e1c)) + +### Testing + +- **postman:** Update postman collection files ([`0248d35`](https://github.com/juspay/hyperswitch/commit/0248d35dd49d2dc7e5e4da6b60a3ee3577c8eac9)) + +### Miscellaneous Tasks + +- Fix channel handling for consumer workflow loop ([#3223](https://github.com/juspay/hyperswitch/pull/3223)) ([`51e1fac`](https://github.com/juspay/hyperswitch/commit/51e1fac556fdd8775e0bbc858b0b3cc50a7e88ec)) + +**Full Changelog:** [`v1.105.0...v1.106.0`](https://github.com/juspay/hyperswitch/compare/v1.105.0...v1.106.0) + +- - - + + ## 1.105.0 (2023-12-23) ### Features From e79604bd4681a69802f3c3169dd94424e3688e42 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 4 Jan 2024 19:18:56 +0530 Subject: [PATCH 266/443] fix(connector): [iatapay] change refund amount (#3244) Co-authored-by: Samraat Bansal --- crates/router/src/connector/iatapay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index fcfdd44e6360..0c156ef08b03 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -479,7 +479,7 @@ impl ConnectorIntegration Date: Fri, 5 Jan 2024 07:20:19 +0000 Subject: [PATCH 267/443] chore(version): v1.106.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b7957272c6..7f7a2ced4db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.106.1 (2024-01-05) + +### Bug Fixes + +- **connector:** [iatapay] change refund amount ([#3244](https://github.com/juspay/hyperswitch/pull/3244)) ([`e79604b`](https://github.com/juspay/hyperswitch/commit/e79604bd4681a69802f3c3169dd94424e3688e42)) + +**Full Changelog:** [`v1.106.0...v1.106.1`](https://github.com/juspay/hyperswitch/compare/v1.106.0...v1.106.1) + +- - - + + ## 1.106.0 (2024-01-04) ### Features From 3ab71fbd5ac86f12cf19d17561e428d33c51a4cf Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:10:26 +0530 Subject: [PATCH 268/443] fix(user): add integration_completed enum in metadata type (#3245) --- .../router/src/utils/user/dashboard_metadata.rs | 15 +++------------ .../down.sql | 2 ++ .../up.sql | 2 ++ 3 files changed, 7 insertions(+), 12 deletions(-) create mode 100644 migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/down.sql create mode 100644 migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs index 40594a6e49f6..09fb5ccd24b4 100644 --- a/crates/router/src/utils/user/dashboard_metadata.rs +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -91,21 +91,12 @@ pub async fn get_merchant_scoped_metadata_from_db( org_id: String, metadata_keys: Vec, ) -> UserResult> { - match state + state .store .find_merchant_scoped_dashboard_metadata(&merchant_id, &org_id, metadata_keys) .await - { - Ok(data) => Ok(data), - Err(e) => { - if e.current_context().is_db_not_found() { - return Ok(Vec::with_capacity(0)); - } - Err(e - .change_context(UserErrors::InternalServerError) - .attach_printable("DB Error Fetching DashboardMetaData")) - } - } + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData") } pub async fn get_user_scoped_metadata_from_db( state: &AppState, diff --git a/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/down.sql b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql new file mode 100644 index 000000000000..bb703fde397f --- /dev/null +++ b/migrations/2024-01-04-121733_add_dashboard_metadata_key_integration_completed/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE "DashboardMetadata" ADD VALUE IF NOT EXISTS 'integration_completed'; \ No newline at end of file From 000e64438838461ea930545405fb2ee0d3c4356c Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:36:25 +0530 Subject: [PATCH 269/443] fix(users): Fix wrong redirection url in magic link (#3217) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/services/email/types.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 9e26c45ba6b1..d5c28b1fd6af 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -77,7 +77,7 @@ pub fn get_link_with_token( token: impl std::fmt::Display, action: impl std::fmt::Display, ) -> String { - format!("{base_url}/user/{action}/?token={token}") + format!("{base_url}/user/{action}?token={token}") } pub struct VerifyEmail { @@ -153,7 +153,8 @@ impl EmailData for MagicLink { .await .change_context(EmailError::TokenGenerationFailure)?; - let magic_link_login = get_link_with_token(&self.settings.email.base_url, token, "login"); + let magic_link_login = + get_link_with_token(&self.settings.email.base_url, token, "verify_email"); let body = html::get_html_body(EmailBody::MagicLink { link: magic_link_login, From 34318bc1f12a1298e8993021a2d516cf86049980 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:47:37 +0530 Subject: [PATCH 270/443] refactor: address panics due to indexing and slicing (#3233) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .cargo/config.toml | 11 +- Cargo.lock | 1 + crates/api_models/src/admin.rs | 16 ++- crates/cards/Cargo.toml | 1 + crates/cards/src/validate.rs | 8 +- crates/common_utils/src/crypto.rs | 5 +- crates/common_utils/src/pii.rs | 53 ++++++--- crates/connector_configs/Cargo.toml | 2 +- .../src/response_modifier.rs | 48 ++++---- crates/euclid/src/dssa/state_machine.rs | 8 +- crates/euclid/src/frontend/dir.rs | 5 +- crates/euclid_macros/src/inner/knowledge.rs | 5 +- .../router/src/connector/authorizedotnet.rs | 7 +- .../connector/authorizedotnet/transformers.rs | 25 ++-- .../src/connector/globalpay/transformers.rs | 2 +- .../src/connector/helcim/transformers.rs | 6 +- .../src/connector/mollie/transformers.rs | 2 +- .../connector/multisafepay/transformers.rs | 2 +- .../src/connector/nexinets/transformers.rs | 14 ++- .../router/src/connector/nmi/transformers.rs | 13 ++- .../src/connector/payeezy/transformers.rs | 2 +- .../src/connector/payme/transformers.rs | 4 +- .../src/connector/placetopay/transformers.rs | 2 +- .../src/connector/powertranz/transformers.rs | 2 +- .../src/connector/prophetpay/transformers.rs | 17 ++- .../router/src/connector/stax/transformers.rs | 2 +- .../src/connector/stripe/transformers.rs | 11 +- .../src/connector/trustpay/transformers.rs | 2 +- .../router/src/connector/tsys/transformers.rs | 2 +- crates/router/src/connector/utils.rs | 28 +++-- .../src/connector/worldline/transformers.rs | 4 +- .../router/src/connector/zen/transformers.rs | 28 ++--- crates/router/src/core/payments/helpers.rs | 8 +- crates/router/src/services/encryption.rs | 109 +----------------- crates/router/src/workflows/payment_sync.rs | 5 +- crates/router/tests/integration_demo.rs | 2 +- crates/router/tests/refunds.rs | 4 +- crates/router_env/src/logger/storage.rs | 15 ++- .../test_utils/tests/connectors/selenium.rs | 16 ++- 39 files changed, 244 insertions(+), 253 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3082e9635cf9..5b27955262ad 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,9 +3,13 @@ rustflags = [ "-Funsafe_code", "-Wclippy::as_conversions", "-Wclippy::expect_used", + "-Wclippy::index_refutable_slice", + "-Wclippy::indexing_slicing", + "-Wclippy::match_on_vec_items", "-Wclippy::missing_panics_doc", - "-Wclippy::panic_in_result_fn", + "-Wclippy::out_of_bounds_indexing", "-Wclippy::panic", + "-Wclippy::panic_in_result_fn", "-Wclippy::panicking_unwrap", "-Wclippy::todo", "-Wclippy::unimplemented", @@ -23,10 +27,7 @@ rustflags = [ [build] -rustdocflags = [ - "--cfg", - "uuid_unstable" -] +rustdocflags = ["--cfg", "uuid_unstable"] [alias] gen-pg = "generate --path ../../../../connector-template -n" diff --git a/Cargo.lock b/Cargo.lock index be1e0c8a2a4e..cd5960a68c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1425,6 +1425,7 @@ dependencies = [ "error-stack", "luhn", "masking", + "router_env", "serde", "serde_json", "thiserror", diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index d35b12152e91..ed49b6f27b5e 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -359,7 +359,7 @@ pub mod payout_routing_algorithm { where A: de::MapAccess<'de>, { - let mut output = serde_json::Value::Object(Map::new()); + let mut output = Map::new(); let mut routing_data: String = "".to_string(); let mut routing_type: String = "".to_string(); @@ -367,14 +367,20 @@ pub mod payout_routing_algorithm { match key { "type" => { routing_type = map.next_value()?; - output["type"] = serde_json::Value::String(routing_type.to_owned()); + output.insert( + "type".to_string(), + serde_json::Value::String(routing_type.to_owned()), + ); } "data" => { routing_data = map.next_value()?; - output["data"] = serde_json::Value::String(routing_data.to_owned()); + output.insert( + "data".to_string(), + serde_json::Value::String(routing_data.to_owned()), + ); } f => { - output[f] = map.next_value()?; + output.insert(f.to_string(), map.next_value()?); } } } @@ -392,7 +398,7 @@ pub mod payout_routing_algorithm { } u => Err(de::Error::custom(format!("Unknown routing algorithm {u}"))), }?; - Ok(output) + Ok(serde_json::Value::Object(output)) } } diff --git a/crates/cards/Cargo.toml b/crates/cards/Cargo.toml index ae72a3d43acc..cf3e25459c6a 100644 --- a/crates/cards/Cargo.toml +++ b/crates/cards/Cargo.toml @@ -19,6 +19,7 @@ time = "0.3.21" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } +router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } [dev-dependencies] serde_json = "1.0.108" diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index d083a420a1e5..001eab3004d7 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -1,6 +1,7 @@ use std::{fmt, ops::Deref, str::FromStr}; use masking::{PeekInterface, Strategy, StrongSecret, WithType}; +use router_env::logger; use serde::{Deserialize, Deserializer, Serialize}; use thiserror::Error; @@ -85,7 +86,12 @@ where return WithType::fmt(val, f); } - write!(f, "{}{}", &val_str[..6], "*".repeat(val_str.len() - 6)) + if let Some(value) = val_str.get(..6) { + write!(f, "{}{}", value, "*".repeat(val_str.len() - 6)) + } else { + logger::error!("Invalid card number {val_str}"); + WithType::fmt(val, f) + } } } diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs index a0f365dd40dd..c2c83a3589e4 100644 --- a/crates/common_utils/src/crypto.rs +++ b/crates/common_utils/src/crypto.rs @@ -279,7 +279,10 @@ impl DecodeMessage for GcmAes256 { .change_context(errors::CryptoError::DecodingFailed)?; let nonce_sequence = NonceSequence::from_bytes( - msg[..ring::aead::NONCE_LEN] + msg.get(..ring::aead::NONCE_LEN) + .ok_or(errors::CryptoError::DecodingFailed) + .into_report() + .attach_printable("Failed to read the nonce form the encrypted ciphertext")? .try_into() .into_report() .change_context(errors::CryptoError::DecodingFailed)?, diff --git a/crates/common_utils/src/pii.rs b/crates/common_utils/src/pii.rs index 39793de5c2b5..1889a5f3aeef 100644 --- a/crates/common_utils/src/pii.rs +++ b/crates/common_utils/src/pii.rs @@ -12,6 +12,8 @@ use diesel::{ }; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret, Strategy, WithType}; +#[cfg(feature = "logs")] +use router_env::logger; use crate::{ crypto::Encryptable, @@ -41,13 +43,14 @@ where fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result { let val_str: &str = val.as_ref(); - // masks everything but the last 4 digits - write!( - f, - "{}{}", - "*".repeat(val_str.len() - 4), - &val_str[val_str.len() - 4..] - ) + if let Some(val_str) = val_str.get(val_str.len() - 4..) { + // masks everything but the last 4 digits + write!(f, "{}{}", "*".repeat(val_str.len() - 4), val_str) + } else { + #[cfg(feature = "logs")] + logger::error!("Invalid phone number: {val_str}"); + WithType::fmt(val, f) + } } } @@ -174,16 +177,26 @@ where { return WithType::fmt(val, f); } - write!( - f, - "{}_{}_{}", - client_secret_segments[0], - client_secret_segments[1], - "*".repeat( - val_str.len() - - (client_secret_segments[0].len() + client_secret_segments[1].len() + 2) + + if let Some((client_secret_segments_0, client_secret_segments_1)) = client_secret_segments + .first() + .zip(client_secret_segments.get(1)) + { + write!( + f, + "{}_{}_{}", + client_secret_segments_0, + client_secret_segments_1, + "*".repeat( + val_str.len() + - (client_secret_segments_0.len() + client_secret_segments_1.len() + 2) + ) ) - ) + } else { + #[cfg(feature = "logs")] + logger::error!("Invalid client secret: {val_str}"); + WithType::fmt(val, f) + } } } @@ -325,7 +338,13 @@ where } } - write!(f, "{}.**.**.**", segments[0]) + if let Some(segments) = segments.first() { + write!(f, "{}.**.**.**", segments) + } else { + #[cfg(feature = "logs")] + logger::error!("Invalid IP address: {val_str}"); + WithType::fmt(val, f) + } } } diff --git a/crates/connector_configs/Cargo.toml b/crates/connector_configs/Cargo.toml index f4df1e6a20b3..083a741ef50f 100644 --- a/crates/connector_configs/Cargo.toml +++ b/crates/connector_configs/Cargo.toml @@ -19,4 +19,4 @@ api_models = { version = "0.1.0", path = "../api_models", package = "api_models" serde = { version = "1.0.193", features = ["derive"] } serde_with = "3.4.0" toml = "0.7.3" -utoipa = { version = "3.3.0", features = ["preserve_order"] } +utoipa = { version = "3.3.0", features = ["preserve_order"] } \ No newline at end of file diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs index 0eb447ace1a9..6a09c58a75ca 100644 --- a/crates/connector_configs/src/response_modifier.rs +++ b/crates/connector_configs/src/response_modifier.rs @@ -279,34 +279,28 @@ impl ConnectorApiIntegrationPayload { pub fn get_google_pay_metadata_response(response: Self) -> Option { match response.metadata { - Some(meta_data) => match meta_data.google_pay { - Some(google_pay) => match google_pay { - GoogleApiModelData::Standard(standard_data) => { - if standard_data.allowed_payment_methods.is_empty() { - None - } else { - let data = Some( - standard_data.allowed_payment_methods[0] - .tokenization_specification - .parameters - .clone(), - ); - match data { - Some(data) => Some(GooglePayData::Standard(GpayDashboardPayLoad { - gateway_merchant_id: data.gateway_merchant_id, - stripe_version: data.stripe_version, - stripe_publishable_key: data.stripe_publishable_key, - merchant_name: standard_data.merchant_info.merchant_name, - merchant_id: standard_data.merchant_info.merchant_id, - })), - None => None, - } + Some(meta_data) => { + match meta_data.google_pay { + Some(google_pay) => match google_pay { + GoogleApiModelData::Standard(standard_data) => { + let data = standard_data.allowed_payment_methods.first().map( + |allowed_pm| { + allowed_pm.tokenization_specification.parameters.clone() + }, + )?; + Some(GooglePayData::Standard(GpayDashboardPayLoad { + gateway_merchant_id: data.gateway_merchant_id, + stripe_version: data.stripe_version, + stripe_publishable_key: data.stripe_publishable_key, + merchant_name: standard_data.merchant_info.merchant_name, + merchant_id: standard_data.merchant_info.merchant_id, + })) } - } - GoogleApiModelData::Zen(data) => Some(GooglePayData::Zen(data)), - }, - None => None, - }, + GoogleApiModelData::Zen(data) => Some(GooglePayData::Zen(data)), + }, + None => None, + } + } None => None, } } diff --git a/crates/euclid/src/dssa/state_machine.rs b/crates/euclid/src/dssa/state_machine.rs index 4cd53911dfe4..93d394eece97 100644 --- a/crates/euclid/src/dssa/state_machine.rs +++ b/crates/euclid/src/dssa/state_machine.rs @@ -678,7 +678,9 @@ mod tests { .collect::>(); assert_eq!( values, - expected_contexts[expected_idx] + expected_contexts + .get(expected_idx) + .expect("Error deriving contexts") .iter() .collect::>() ); @@ -702,7 +704,9 @@ mod tests { .collect::>(); assert_eq!( values, - expected_contexts[expected_idx] + expected_contexts + .get(expected_idx) + .expect("Error deriving contexts") .iter() .collect::>() ); diff --git a/crates/euclid/src/frontend/dir.rs b/crates/euclid/src/frontend/dir.rs index f8cef1f92955..dc81359c51d5 100644 --- a/crates/euclid/src/frontend/dir.rs +++ b/crates/euclid/src/frontend/dir.rs @@ -722,7 +722,10 @@ mod test { }; let display_str = key.to_string(); - assert_eq!(&json_str[1..json_str.len() - 1], display_str); + assert_eq!( + json_str.get(1..json_str.len() - 1).expect("Value metadata"), + display_str + ); key_names.insert(key, display_str); } diff --git a/crates/euclid_macros/src/inner/knowledge.rs b/crates/euclid_macros/src/inner/knowledge.rs index 73b94919c903..9f33a6871c56 100644 --- a/crates/euclid_macros/src/inner/knowledge.rs +++ b/crates/euclid_macros/src/inner/knowledge.rs @@ -417,7 +417,10 @@ impl GenContext { .position(|v| *v == node_id) .ok_or_else(|| "Error deciding cycle order".to_string())?; - let cycle_order = order[position..].to_vec(); + let cycle_order = order + .get(position..) + .ok_or_else(|| "Error getting cycle order".to_string())? + .to_vec(); Ok(Some(cycle_order)) } else if visited.contains(&node_id) { Ok(None) diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 9c8d2f470246..0686ee6a3b86 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -903,7 +903,12 @@ fn get_error_response( })), Some(authorizedotnet::TransactionResponse::AuthorizedotnetTransactionResponseError(_)) | None => { - let message = &response.messages.message[0].text; + let message = &response + .messages + .message + .first() + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? + .text; Ok(types::ErrorResponse { code: consts::NO_ERROR_CODE.to_string(), message: message.to_string(), diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 30323ca4ef23..96cf3b6ffc51 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -619,7 +619,7 @@ impl Some(TransactionResponse::AuthorizedotnetTransactionResponseError(_)) | None => { Ok(Self { status: enums::AttemptStatus::Failure, - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }) } @@ -689,7 +689,7 @@ impl } None => Ok(Self { status: enums::AttemptStatus::Failure, - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }), } @@ -944,7 +944,7 @@ impl TryFrom Ok(Self { - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }), } @@ -986,7 +986,7 @@ impl }) } None => Ok(Self { - response: Err(get_err_response(item.http_code, item.response.messages)), + response: Err(get_err_response(item.http_code, item.response.messages)?), ..item.data }), } @@ -1024,15 +1024,22 @@ impl From> for TransactionType { } } -fn get_err_response(status_code: u16, message: ResponseMessages) -> types::ErrorResponse { - types::ErrorResponse { - code: message.message[0].code.clone(), - message: message.message[0].text.clone(), +fn get_err_response( + status_code: u16, + message: ResponseMessages, +) -> Result { + let response_message = message + .message + .first() + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(types::ErrorResponse { + code: response_message.code.clone(), + message: response_message.text.clone(), reason: None, status_code, attempt_status: None, connector_transaction_id: None, - } + }) } #[derive(Debug, Deserialize)] diff --git a/crates/router/src/connector/globalpay/transformers.rs b/crates/router/src/connector/globalpay/transformers.rs index 9cef564b3795..3124501bd6e5 100644 --- a/crates/router/src/connector/globalpay/transformers.rs +++ b/crates/router/src/connector/globalpay/transformers.rs @@ -385,7 +385,7 @@ fn get_payment_method_data( api::PaymentMethodData::Card(ccard) => Ok(PaymentMethodData::Card(requests::Card { number: ccard.card_number.clone(), expiry_month: ccard.card_exp_month.clone(), - expiry_year: ccard.get_card_expiry_year_2_digit(), + expiry_year: ccard.get_card_expiry_year_2_digit()?, cvv: ccard.card_cvc.clone(), account_type: None, authcode: None, diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 823096d66482..599054163c3d 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -126,7 +126,8 @@ impl TryFrom<(&types::SetupMandateRouterData, &api::Card)> for HelcimVerifyReque fn try_from(value: (&types::SetupMandateRouterData, &api::Card)) -> Result { let (item, req_card) = value; let card_data = HelcimCard { - card_expiry: req_card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + card_expiry: req_card + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, card_number: req_card.card_number.clone(), card_c_v_v: req_card.card_cvc.clone(), }; @@ -196,7 +197,8 @@ impl ) -> Result { let (item, req_card) = value; let card_data = HelcimCard { - card_expiry: req_card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + card_expiry: req_card + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, card_number: req_card.card_number.clone(), card_c_v_v: req_card.card_cvc.clone(), }; diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs index 5960e9cdb8d9..0fcb14d2cf59 100644 --- a/crates/router/src/connector/mollie/transformers.rs +++ b/crates/router/src/connector/mollie/transformers.rs @@ -292,7 +292,7 @@ impl TryFrom<&types::TokenizationRouterData> for MollieCardTokenRequest { .unwrap_or(Secret::new("".to_string())); let card_number = ccard.card_number.clone(); let card_expiry_date = - ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()); + ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?; let card_cvv = ccard.card_cvc; let locale = item.request.get_browser_info()?.get_language()?; let testmode = diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index 0a034724a629..86096ed508b6 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -426,7 +426,7 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> card_expiry_date: Some( (format!( "{}{}", - ccard.get_card_expiry_year_2_digit().expose(), + ccard.get_card_expiry_year_2_digit()?.expose(), ccard.card_exp_month.clone().expose() )) .parse::() diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 8875abdb7868..698b2709a62d 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -643,7 +643,7 @@ fn get_card_data( Some(true) => CardDataDetails::PaymentInstrument(Box::new(PaymentInstrument { payment_instrument_id: item.request.connector_mandate_id(), })), - _ => CardDataDetails::CardDetails(Box::new(get_card_details(card))), + _ => CardDataDetails::CardDetails(Box::new(get_card_details(card)?)), }; let cof_contract = Some(CofContract { recurring_type: RecurringType::Unscheduled, @@ -651,7 +651,7 @@ fn get_card_data( (card_data, cof_contract) } false => ( - CardDataDetails::CardDetails(Box::new(get_card_details(card))), + CardDataDetails::CardDetails(Box::new(get_card_details(card)?)), None, ), }; @@ -677,13 +677,15 @@ fn get_applepay_details( }) } -fn get_card_details(req_card: &api_models::payments::Card) -> CardDetails { - CardDetails { +fn get_card_details( + req_card: &api_models::payments::Card, +) -> Result { + Ok(CardDetails { card_number: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), - expiry_year: req_card.get_card_expiry_year_2_digit(), + expiry_year: req_card.get_card_expiry_year_2_digit()?, verification: req_card.card_cvc.clone(), - } + }) } fn get_wallet_details( diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 931dac5a9664..b9ad5b8e1883 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -127,7 +127,7 @@ fn get_card_details( utils::CardData::get_card_expiry_month_year_2_digit_with_delimiter( card_details, "".to_string(), - ), + )?, card_details.card_cvc.clone(), )), _ => Err(errors::ConnectorError::NotImplemented( @@ -459,7 +459,7 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { payment_method_data: &api_models::payments::PaymentMethodData, ) -> Result { match &payment_method_data { - api::PaymentMethodData::Card(ref card) => Ok(Self::from(card)), + api::PaymentMethodData::Card(ref card) => Ok(Self::try_from(card)?), api::PaymentMethodData::Wallet(ref wallet_type) => match wallet_type { api_models::payments::WalletData::GooglePay(ref googlepay_data) => { Ok(Self::from(googlepay_data)) @@ -518,18 +518,19 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { } } -impl From<&api_models::payments::Card> for PaymentMethod { - fn from(card: &api_models::payments::Card) -> Self { +impl TryFrom<&api_models::payments::Card> for PaymentMethod { + type Error = Error; + fn try_from(card: &api_models::payments::Card) -> Result { let ccexp = utils::CardData::get_card_expiry_month_year_2_digit_with_delimiter( card, "".to_string(), - ); + )?; let card = CardData { ccnumber: card.card_number.clone(), ccexp, cvv: card.card_cvc.clone(), }; - Self::Card(Box::new(card)) + Ok(Self::Card(Box::new(card))) } } diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 7ae7feba68af..c633fd3d99f9 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -246,7 +246,7 @@ fn get_payment_method_data( .clone() .unwrap_or(Secret::new("".to_string())), card_number: card.card_number.clone(), - exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, cvv: card.card_cvc.clone(), }; Ok(PayeezyPaymentMethod::PayeezyCard(payeezy_card)) diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index a865b8fec3a9..a7b21ce292de 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -648,7 +648,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { let card = PaymeCard { credit_card_cvv: req_card.card_cvc.clone(), credit_card_exp: req_card - .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, credit_card_number: req_card.card_number, }; let buyer_email = item.request.get_email()?; @@ -755,7 +755,7 @@ impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { let card = PaymeCard { credit_card_cvv: req_card.card_cvc.clone(), credit_card_exp: req_card - .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, credit_card_number: req_card.card_number, }; Ok(Self { diff --git a/crates/router/src/connector/placetopay/transformers.rs b/crates/router/src/connector/placetopay/transformers.rs index 7fef8b8954ba..dfdd3f904df4 100644 --- a/crates/router/src/connector/placetopay/transformers.rs +++ b/crates/router/src/connector/placetopay/transformers.rs @@ -131,7 +131,7 @@ impl TryFrom<&PlacetopayRouterData<&types::PaymentsAuthorizeRouterData>> number: req_card.card_number.clone(), expiration: req_card .clone() - .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?, cvv: req_card.card_cvc.clone(), }; Ok(Self { diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 005496332008..8dc3688da9eb 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -220,7 +220,7 @@ impl TryFrom<&Card> for Source { .clone() .unwrap_or(Secret::new("".to_string())), card_pan: card.card_number.clone(), - card_expiration: card.get_expiry_date_as_yymm(), + card_expiration: card.get_expiry_date_as_yymm()?, card_cvv: card.card_cvc.clone(), }; Ok(Self::Card(card)) diff --git a/crates/router/src/connector/prophetpay/transformers.rs b/crates/router/src/connector/prophetpay/transformers.rs index d05f2c3986a7..23cffa3da229 100644 --- a/crates/router/src/connector/prophetpay/transformers.rs +++ b/crates/router/src/connector/prophetpay/transformers.rs @@ -293,10 +293,19 @@ fn get_card_token( let values = param.peek().split('&').collect::>(); for value in values { let pair = value.split('=').collect::>(); - queries.insert(pair[0].to_string(), pair[1].to_string()); + queries.insert( + pair.first() + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? + .to_string(), + pair.get(1) + .ok_or(errors::ConnectorError::ResponseDeserializationFailed)? + .to_string(), + ); } - queries + Ok(queries) }) + .transpose() + .into_report()? .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; for (key, val) in queries_params { @@ -307,8 +316,8 @@ fn get_card_token( Err(errors::ConnectorError::MissingRequiredField { field_name: "card_token", - }) - .into_report() + } + .into()) } #[derive(Debug, Clone, Serialize)] diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 081be000cf6c..01ae751f7487 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -225,7 +225,7 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { api::PaymentMethodData::Card(card_data) => { let stax_card_data = StaxTokenizeData { card_exp: card_data - .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?, person_name: card_data .card_holder_name .unwrap_or(Secret::new("".to_string())), diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 3a28f777907f..b044331379a1 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -3649,7 +3649,10 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.first_name"); + assert_eq!( + *missing_fields.first().unwrap(), + "shipping.address.first_name" + ); } #[test] @@ -3674,7 +3677,7 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.line1"); + assert_eq!(*missing_fields.first().unwrap(), "shipping.address.line1"); } #[test] @@ -3699,7 +3702,7 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.country"); + assert_eq!(*missing_fields.first().unwrap(), "shipping.address.country"); } #[test] @@ -3723,7 +3726,7 @@ mod test_validate_shipping_address_against_payment_method { assert!(result.is_err()); let missing_fields = get_missing_fields(result.unwrap_err().current_context()).to_owned(); assert_eq!(missing_fields.len(), 1); - assert_eq!(missing_fields[0], "shipping.address.zip"); + assert_eq!(*missing_fields.first().unwrap(), "shipping.address.zip"); } #[test] diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 8ae12622fb06..87d98c1b1bee 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -297,7 +297,7 @@ fn get_card_request_data( currency: item.request.currency.to_string(), pan: ccard.card_number.clone(), cvv: ccard.card_cvc.clone(), - expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?, cardholder: get_full_name(params.billing_first_name, billing_last_name), reference: item.connector_request_reference_id.clone(), redirect_url: return_url, diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index 8c9c6cd43df4..dd700d11bcbe 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -52,7 +52,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { currency_code: item.request.currency, card_number: ccard.card_number.clone(), expiration_date: ccard - .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + .get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned())?, cvv2: ccard.card_cvc, terminal_capability: "ICC_CHIP_READ_ONLY".to_string(), terminal_operating_environment: "ON_MERCHANT_PREMISES_ATTENDED".to_string(), diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 9a538a7207e9..24def6253726 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -757,23 +757,27 @@ pub enum CardIssuer { } pub trait CardData { - fn get_card_expiry_year_2_digit(&self) -> Secret; + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError>; fn get_card_issuer(&self) -> Result; fn get_card_expiry_month_year_2_digit_with_delimiter( &self, delimiter: String, - ) -> Secret; + ) -> Result, errors::ConnectorError>; fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret; fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret; fn get_expiry_year_4_digit(&self) -> Secret; - fn get_expiry_date_as_yymm(&self) -> Secret; + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError>; } impl CardData for api::Card { - fn get_card_expiry_year_2_digit(&self) -> Secret { + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError> { let binding = self.card_exp_year.clone(); let year = binding.peek(); - Secret::new(year[year.len() - 2..].to_string()) + Ok(Secret::new( + year.get(year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(), + )) } fn get_card_issuer(&self) -> Result { get_card_issuer(self.card_number.peek()) @@ -781,14 +785,14 @@ impl CardData for api::Card { fn get_card_expiry_month_year_2_digit_with_delimiter( &self, delimiter: String, - ) -> Secret { - let year = self.get_card_expiry_year_2_digit(); - Secret::new(format!( + ) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?; + Ok(Secret::new(format!( "{}{}{}", self.card_exp_month.peek().clone(), delimiter, year.peek() - )) + ))) } fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret { let year = self.get_expiry_year_4_digit(); @@ -815,10 +819,10 @@ impl CardData for api::Card { } Secret::new(year) } - fn get_expiry_date_as_yymm(&self) -> Secret { - let year = self.get_card_expiry_year_2_digit().expose(); + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); let month = self.card_exp_month.clone().expose(); - Secret::new(format!("{year}{month}")) + Ok(Secret::new(format!("{year}{month}"))) } } diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index b657756b6a86..c00913aa57d1 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -346,7 +346,9 @@ fn make_card_request( let secret_value = format!( "{}{}", ccard.card_exp_month.peek(), - &expiry_year[expiry_year.len() - 2..] + &expiry_year + .get(expiry_year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? ); let expiry_date: Secret = Secret::new(secret_value); let card = Card { diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index c66b098fe751..7ea6953a3f2e 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -222,7 +222,7 @@ impl TryFrom<(&ZenRouterData<&types::PaymentsAuthorizeRouterData>, &Card)> for Z card: Some(ZenCardDetails { number: ccard.card_number.clone(), expiry_date: ccard - .get_card_expiry_month_year_2_digit_with_delimiter("".to_owned()), + .get_card_expiry_month_year_2_digit_with_delimiter("".to_owned())?, cvv: ccard.card_cvc.clone(), }), descriptor: item @@ -538,7 +538,7 @@ fn get_checkout_signature( .pay_wall_secret .clone() .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - let mut signature_data = get_signature_data(checkout_request); + let mut signature_data = get_signature_data(checkout_request)?; signature_data.push_str(&pay_wall_secret); let payload_digest = digest::digest(&digest::SHA256, signature_data.as_bytes()); let mut signature = hex::encode(payload_digest); @@ -547,7 +547,9 @@ fn get_checkout_signature( } /// Fields should be in alphabetical order -fn get_signature_data(checkout_request: &CheckoutRequest) -> String { +fn get_signature_data( + checkout_request: &CheckoutRequest, +) -> Result { let specified_payment_channel = match checkout_request.specified_payment_channel { ZenPaymentChannels::PclCard => "pcl_card", ZenPaymentChannels::PclGooglepay => "pcl_googlepay", @@ -568,21 +570,19 @@ fn get_signature_data(checkout_request: &CheckoutRequest) -> String { ]; for index in 0..checkout_request.items.len() { let prefix = format!("items[{index}]."); + let checkout_request_items = checkout_request + .items + .get(index) + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; signature_data.push(format!( "{prefix}lineamounttotal={}", - checkout_request.items[index].line_amount_total - )); - signature_data.push(format!( - "{prefix}name={}", - checkout_request.items[index].name - )); - signature_data.push(format!( - "{prefix}price={}", - checkout_request.items[index].price + checkout_request_items.line_amount_total )); + signature_data.push(format!("{prefix}name={}", checkout_request_items.name)); + signature_data.push(format!("{prefix}price={}", checkout_request_items.price)); signature_data.push(format!( "{prefix}quantity={}", - checkout_request.items[index].quantity + checkout_request_items.quantity )); } signature_data.push(format!( @@ -598,7 +598,7 @@ fn get_signature_data(checkout_request: &CheckoutRequest) -> String { )); signature_data.push(format!("urlredirect={}", checkout_request.url_redirect)); let signature = signature_data.join("&"); - signature.to_lowercase() + Ok(signature.to_lowercase()) } fn get_customer( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 2cd62fbd4914..08e2e5dab1ae 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3621,8 +3621,12 @@ impl ApplePayData { .into_report() .change_context(errors::ApplePayDecryptionError::Base64DecodingFailed)?; let iv = [0u8; 16]; //Initialization vector IV is typically used in AES-GCM (Galois/Counter Mode) encryption for randomizing the encryption process. - let ciphertext = &data[..data.len() - 16]; - let tag = &data[data.len() - 16..]; + let ciphertext = data + .get(..data.len() - 16) + .ok_or(errors::ApplePayDecryptionError::DecryptionFailed)?; + let tag = data + .get(data.len() - 16..) + .ok_or(errors::ApplePayDecryptionError::DecryptionFailed)?; let cipher = Cipher::aes_256_gcm(); let decrypted_data = decrypt_aead(cipher, symmetric_key, Some(&iv), &[], ciphertext, tag) .into_report() diff --git a/crates/router/src/services/encryption.rs b/crates/router/src/services/encryption.rs index acea96f86607..9a8a5f4af4a9 100644 --- a/crates/router/src/services/encryption.rs +++ b/crates/router/src/services/encryption.rs @@ -1,9 +1,7 @@ -use std::{num::Wrapping, str}; +use std::str; use error_stack::{report, IntoReport, ResultExt}; use josekit::{jwe, jws}; -use rand; -use ring::{aead::*, error::Unspecified}; use serde::{Deserialize, Serialize}; use crate::{ @@ -11,44 +9,6 @@ use crate::{ utils, }; -struct NonceGen { - current: Wrapping, - start: u128, -} - -impl NonceGen { - fn new(start: [u8; 12]) -> Self { - let mut array = [0; 16]; - array[..12].copy_from_slice(&start); - let start = if cfg!(target_endian = "little") { - u128::from_le_bytes(array) - } else { - u128::from_be_bytes(array) - }; - Self { - current: Wrapping(start), - start, - } - } -} - -impl NonceSequence for NonceGen { - fn advance(&mut self) -> Result { - let n = self.current.0; - self.current += 1; - if self.current.0 == self.start { - return Err(Unspecified); - } - let value = if cfg!(target_endian = "little") { - n.to_le_bytes()[..12].try_into()? - } else { - n.to_be_bytes()[..12].try_into()? - }; - let nonce = Nonce::assume_unique_for_key(value); - Ok(nonce) - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JwsBody { pub header: String, @@ -66,57 +26,6 @@ pub struct JweBody { pub encrypted_key: String, } -pub fn encrypt(msg: &String, key: &[u8]) -> CustomResult, errors::EncryptionError> { - let nonce_seed = rand::random(); - let mut sealing_key = { - let key = UnboundKey::new(&AES_256_GCM, key) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Unbound Key Error")?; - let nonce_gen = NonceGen::new(nonce_seed); - SealingKey::new(key, nonce_gen) - }; - let msg_byte = msg.as_bytes(); - let mut data = msg_byte.to_vec(); - - sealing_key - .seal_in_place_append_tag(Aad::empty(), &mut data) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Error Encrypting")?; - let nonce_vec = nonce_seed.to_vec(); - data.splice(0..0, nonce_vec); //prepend nonce at the start - Ok(data) -} - -pub fn decrypt(mut data: Vec, key: &[u8]) -> CustomResult { - let nonce_seed = data[0..12] - .try_into() - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Error getting nonce")?; - data.drain(0..12); - - let mut opening_key = { - let key = UnboundKey::new(&AES_256_GCM, key) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Unbound Key Error")?; - let nonce_gen = NonceGen::new(nonce_seed); - OpeningKey::new(key, nonce_gen) - }; - let res_byte = opening_key - .open_in_place(Aad::empty(), &mut data) - .map_err(errors::EncryptionError::from) - .into_report() - .attach_printable("Error Decrypting")?; - let response = str::from_utf8_mut(res_byte) - .into_report() - .change_context(errors::EncryptionError) - .attach_printable("Error from_utf8")?; - Ok(response.to_string()) -} - pub async fn encrypt_jwe( payload: &[u8], public_key: impl AsRef<[u8]>, @@ -220,7 +129,6 @@ mod tests { #![allow(clippy::unwrap_used, clippy::expect_used)] use super::*; - use crate::utils::{self, ValueExt}; // Keys used for tests // Can be generated using the following commands: @@ -308,21 +216,6 @@ VuY3OeNxi+dC2r7HppP3O/MJ4gX/RJJfSrcaGP8/Ke1W5+jE97Qy -----END RSA PRIVATE KEY----- "; - fn generate_key() -> [u8; 32] { - let key: [u8; 32] = rand::random(); - key - } - - #[test] - fn test_enc() { - let key = generate_key(); - let enc_data = encrypt(&"Test_Encrypt".to_string(), &key).unwrap(); - let card_info = utils::Encode::>::encode_to_value(&enc_data).unwrap(); - let data: Vec = card_info.parse_value("ParseEncryptedData").unwrap(); - let dec_data = decrypt(data, &key).unwrap(); - assert_eq!(dec_data, "Test_Encrypt".to_string()); - } - #[actix_rt::test] async fn test_jwe() { let jwt = encrypt_jwe("request_payload".as_bytes(), ENCRYPTION_KEY) diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 43567ce27e23..b2296e17f70d 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -301,7 +301,10 @@ mod tests { let cpt_default = process_data::ConnectorPTMapping::default().default_mapping; assert_eq!( vec![schedule_time_delta, first_retry_time_delta], - vec![cpt_default.start_after, cpt_default.frequency[0]] + vec![ + cpt_default.start_after, + *cpt_default.frequency.first().unwrap() + ] ); } } diff --git a/crates/router/tests/integration_demo.rs b/crates/router/tests/integration_demo.rs index 5d2c4a7943b4..777e0a682985 100644 --- a/crates/router/tests/integration_demo.rs +++ b/crates/router/tests/integration_demo.rs @@ -153,7 +153,7 @@ async fn exceed_refund() { let message: serde_json::Value = user_client.create_refund(&server, &payment_id, 100).await; assert_eq!( - message["error"]["message"], + message.get("error").unwrap().get("message").unwrap(), "The refund amount exceeds the amount captured." ); } diff --git a/crates/router/tests/refunds.rs b/crates/router/tests/refunds.rs index 6b9dfd5ed4a2..8d6a158cdff2 100644 --- a/crates/router/tests/refunds.rs +++ b/crates/router/tests/refunds.rs @@ -19,7 +19,7 @@ async fn refund_create_fail_stripe() { let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let refund: serde_json::Value = user_client.create_refund(&app, &payment_id, 10).await; - assert_eq!(refund["error"]["message"], "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); + assert_eq!(refund.get("error").unwrap().get("message").unwrap(), "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); } #[actix_web::test] @@ -33,7 +33,7 @@ async fn refund_create_fail_adyen() { let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let refund: serde_json::Value = user_client.create_refund(&app, &payment_id, 10).await; - assert_eq!(refund["error"]["message"], "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); + assert_eq!(refund.get("error").unwrap().get("message").unwrap(), "Access forbidden, invalid API key was used. Please create your new API key from the Dashboard Settings section."); } #[actix_web::test] diff --git a/crates/router_env/src/logger/storage.rs b/crates/router_env/src/logger/storage.rs index 04f00f71cb3e..51e701213b9c 100644 --- a/crates/router_env/src/logger/storage.rs +++ b/crates/router_env/src/logger/storage.rs @@ -77,8 +77,12 @@ impl Visit for Storage<'_> { // Skip fields which are already handled name if name.starts_with("log.") => (), name if name.starts_with("r#") => { - self.values - .insert(&name[2..], serde_json::Value::from(format!("{value:?}"))); + self.values.insert( + #[allow(clippy::expect_used)] + name.get(2..) + .expect("field name must have a minimum of two characters"), + serde_json::Value::from(format!("{value:?}")), + ); } name => { self.values @@ -88,12 +92,12 @@ impl Visit for Storage<'_> { } } -#[allow(clippy::expect_used)] impl tracing_subscriber::registry::LookupSpan<'a>> Layer for StorageSubscription { /// On new span. fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(id).expect("No span"); let mut visitor = if let Some(parent_span) = span.parent() { @@ -113,8 +117,10 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer /// On additional key value pairs store it. fn on_record(&self, span: &Id, values: &Record<'_>, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(span).expect("No span"); let mut extensions = span.extensions_mut(); + #[allow(clippy::expect_used)] let visitor = extensions .get_mut::>() .expect("The span does not have storage"); @@ -123,6 +129,7 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer /// On enter store time. fn on_enter(&self, span: &Id, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(span).expect("No span"); let mut extensions = span.extensions_mut(); if extensions.get_mut::().is_none() { @@ -132,6 +139,7 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer /// On close create an entry about how long did it take. fn on_close(&self, span: Id, ctx: Context<'_, S>) { + #[allow(clippy::expect_used)] let span = ctx.span(&span).expect("No span"); let elapsed_milliseconds = { @@ -143,6 +151,7 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer }; let mut extensions_mut = span.extensions_mut(); + #[allow(clippy::expect_used)] let visitor = extensions_mut .get_mut::>() .expect("No visitor in extensions"); diff --git a/crates/test_utils/tests/connectors/selenium.rs b/crates/test_utils/tests/connectors/selenium.rs index 865cd950f764..303d4cd7ccbf 100644 --- a/crates/test_utils/tests/connectors/selenium.rs +++ b/crates/test_utils/tests/connectors/selenium.rs @@ -397,7 +397,7 @@ pub trait SeleniumTest { ) -> Result<(), WebDriverError> { let config = self.get_configs().automation_configs.unwrap(); if config.run_minimum_steps.unwrap() { - self.complete_actions(&web_driver, actions[..3].to_vec()) + self.complete_actions(&web_driver, actions.get(..3).unwrap().to_vec()) .await } else { self.complete_actions(&web_driver, actions).await @@ -538,7 +538,7 @@ pub trait SeleniumTest { let response = client.get(outgoing_webhook_url).send().await.unwrap(); // get events from outgoing webhook endpoint let body_text = response.text().await.unwrap(); let data: WebhookResponse = serde_json::from_str(&body_text).unwrap(); - let last_three_events = &data.data[data.data.len().saturating_sub(3)..]; // Get the last three elements if available + let last_three_events = data.data.get(data.data.len().saturating_sub(3)..).unwrap(); // Get the last three elements if available for last_event in last_three_events { let last_event_body = &last_event.step.request.body; let decoded_bytes = base64::engine::general_purpose::STANDARD //decode the encoded outgoing webhook event @@ -762,7 +762,7 @@ macro_rules! function { std::any::type_name::() } let name = type_name_of(f); - &name[..name.len() - 3] + &name.get(..name.len() - 3).unwrap() }}; } @@ -812,8 +812,14 @@ pub fn should_ignore_test(name: &str) -> bool { let tests_to_ignore: HashSet = serde_json::from_value(conf).unwrap_or_else(|_| HashSet::new()); let modules: Vec<_> = name.split("::").collect(); - let file_match = format!("{}::*", <&str>::clone(&modules[1])); - let module_name = modules[1..3].join("::"); + let file_match = format!( + "{}::*", + <&str>::clone(modules.get(1).expect("Error obtaining module path segment")) + ); + let module_name = modules + .get(1..3) + .expect("Error obtaining module path segment") + .join("::"); // Ignore if it matches patterns like nuvei_ui::*, nuvei_ui::should_make_nuvei_eps_payment_test tests_to_ignore.contains(&file_match) || tests_to_ignore.contains(&module_name) } From 1d26df28bc5e1db359272b40adae70bfba9b7360 Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:01:56 +0530 Subject: [PATCH 271/443] feat(analytics): adding outgoing webhooks kafka event (#3140) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 13 ++- config/development.toml | 1 + config/docker_compose.toml | 1 + .../scripts/outgoing_webhook_events.sql | 109 +++++++++++++++++ crates/router/src/core/errors.rs | 2 +- crates/router/src/core/webhooks.rs | 37 +++++- crates/router/src/events.rs | 2 + .../src/events/outgoing_webhook_logs.rs | 110 ++++++++++++++++++ crates/router/src/services/kafka.rs | 4 + 9 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql create mode 100644 crates/router/src/events/outgoing_webhook_logs.rs diff --git a/config/config.example.toml b/config/config.example.toml index f9ae71d3b9ee..21a3ba6de93c 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -524,9 +524,10 @@ enabled = true # Switch to enable or disable PayPal onboarding source = "logs" # The event sink to push events supports kafka or logs (stdout) [events.kafka] -brokers = [] # Kafka broker urls for bootstrapping the client -intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events -attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events -refund_analytics_topic = "topic" # Kafka topic to be used for Refund events -api_logs_topic = "topic" # Kafka topic to be used for incoming api events -connector_logs_topic = "topic" # Kafka topic to be used for connector api events \ No newline at end of file +brokers = [] # Kafka broker urls for bootstrapping the client +intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events +attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events +refund_analytics_topic = "topic" # Kafka topic to be used for Refund events +api_logs_topic = "topic" # Kafka topic to be used for incoming api events +connector_logs_topic = "topic" # Kafka topic to be used for connector api events +outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events diff --git a/config/development.toml b/config/development.toml index 6e7e040906a5..80d594b248bd 100644 --- a/config/development.toml +++ b/config/development.toml @@ -519,6 +519,7 @@ attempt_analytics_topic = "hyperswitch-payment-attempt-events" refund_analytics_topic = "hyperswitch-refund-events" api_logs_topic = "hyperswitch-api-log-events" connector_logs_topic = "hyperswitch-connector-api-events" +outgoing_webhook_logs_topic = "hyperswitch-outgoing-webhook-events" [analytics] source = "sqlx" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 63be7339c7bc..1d3c845aa54f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -366,6 +366,7 @@ attempt_analytics_topic = "hyperswitch-payment-attempt-events" refund_analytics_topic = "hyperswitch-refund-events" api_logs_topic = "hyperswitch-api-log-events" connector_logs_topic = "hyperswitch-connector-api-events" +outgoing_webhook_logs_topic = "hyperswitch-outgoing-webhook-events" [analytics] source = "sqlx" diff --git a/crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql b/crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql new file mode 100644 index 000000000000..3dc907629d0a --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/outgoing_webhook_events.sql @@ -0,0 +1,109 @@ +CREATE TABLE + outgoing_webhook_events_queue ( + `merchant_id` String, + `event_id` Nullable(String), + `event_type` LowCardinality(String), + `outgoing_webhook_event_type` LowCardinality(String), + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `attempt_id` Nullable(String), + `dispute_id` Nullable(String), + `payment_method_id` Nullable(String), + `mandate_id` Nullable(String), + `content` Nullable(String), + `is_error` Bool, + `error` Nullable(String), + `created_at_timestamp` DateTime64(3) + ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', + kafka_topic_list = 'hyperswitch-outgoing-webhook-events', + kafka_group_name = 'hyper-c1', + kafka_format = 'JSONEachRow', + kafka_handle_error_mode = 'stream'; + +CREATE TABLE + outgoing_webhook_events_cluster ( + `merchant_id` String, + `event_id` String, + `event_type` LowCardinality(String), + `outgoing_webhook_event_type` LowCardinality(String), + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `attempt_id` Nullable(String), + `dispute_id` Nullable(String), + `payment_method_id` Nullable(String), + `mandate_id` Nullable(String), + `content` Nullable(String), + `is_error` Bool, + `error` Nullable(String), + `created_at_timestamp` DateTime64(3), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), + INDEX eventIndex event_type TYPE bloom_filter GRANULARITY 1, + INDEX webhookeventIndex outgoing_webhook_event_type TYPE bloom_filter GRANULARITY 1 + ) ENGINE = MergeTree PARTITION BY toStartOfDay(created_at_timestamp) +ORDER BY ( + created_at_timestamp, + merchant_id, + event_id, + event_type, + outgoing_webhook_event_type + ) TTL inserted_at + toIntervalMonth(6); + +CREATE MATERIALIZED VIEW outgoing_webhook_events_mv TO outgoing_webhook_events_cluster ( + `merchant_id` String, + `event_id` Nullable(String), + `event_type` LowCardinality(String), + `outgoing_webhook_event_type` LowCardinality(String), + `payment_id` Nullable(String), + `refund_id` Nullable(String), + `attempt_id` Nullable(String), + `dispute_id` Nullable(String), + `payment_method_id` Nullable(String), + `mandate_id` Nullable(String), + `content` Nullable(String), + `is_error` Bool, + `error` Nullable(String), + `created_at_timestamp` DateTime64(3), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), +) AS +SELECT + merchant_id, + event_id, + event_type, + outgoing_webhook_event_type, + payment_id, + refund_id, + attempt_id, + dispute_id, + payment_method_id, + mandate_id, + content, + is_error, + error, + created_at_timestamp, + now() AS inserted_at +FROM + outgoing_webhook_events_queue +where length(_error) = 0; + +CREATE MATERIALIZED VIEW outgoing_webhook_parse_errors ( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) ENGINE = MergeTree +ORDER BY ( + topic, partition, + offset + ) SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS +offset +, + _raw_message AS raw, + _error AS error +FROM + outgoing_webhook_events_queue +WHERE length(_error) > 0; \ No newline at end of file diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 054f4053504e..cbc4290f63bb 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -226,7 +226,7 @@ pub enum KmsError { Utf8DecodingFailed, } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, serde::Serialize)] pub enum WebhooksFlowError { #[error("Merchant webhook config not found")] MerchantConfigNotFound, diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 762ee19b6415..c7e7548f00bd 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -25,7 +25,10 @@ use crate::{ payments, refunds, }, db::StorageInterface, - events::api_logs::ApiEvent, + events::{ + api_logs::ApiEvent, + outgoing_webhook_logs::{OutgoingWebhookEvent, OutgoingWebhookEventMetric}, + }, logger, routes::{app::AppStateInfo, lock_utils, metrics::request::add_attributes, AppState}, services::{self, authentication as auth}, @@ -731,21 +734,47 @@ pub async fn create_event_and_trigger_outgoing_webhook(business_profile, outgoing_webhook, state).await; if let Err(e) = result { + error.replace( + serde_json::to_value(e.current_context()) + .into_report() + .attach_printable("Failed to serialize json error response") + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .ok() + .into(), + ); logger::error!(?e); } + let outgoing_webhook_event_type = content.get_outgoing_webhook_event_type(); + let webhook_event = OutgoingWebhookEvent::new( + merchant_account.merchant_id.clone(), + event.event_id.clone(), + event_type, + outgoing_webhook_event_type, + error.is_some(), + error, + ); + match webhook_event.clone().try_into() { + Ok(event) => { + state_clone.event_handler().log_event(event); + } + Err(err) => { + logger::error!(error=?err, event=?webhook_event, "Error Logging Outgoing Webhook Event"); + } + } }); } diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index 2dc9258e19df..0091de588f13 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -9,6 +9,7 @@ pub mod api_logs; pub mod connector_api_logs; pub mod event_logger; pub mod kafka_handler; +pub mod outgoing_webhook_logs; pub(super) trait EventHandler: Sync + Send + dyn_clone::DynClone { fn log_event(&self, event: RawEvent); @@ -31,6 +32,7 @@ pub enum EventType { Refund, ApiLogs, ConnectorApiLogs, + OutgoingWebhookLogs, } #[derive(Debug, Default, Deserialize, Clone)] diff --git a/crates/router/src/events/outgoing_webhook_logs.rs b/crates/router/src/events/outgoing_webhook_logs.rs new file mode 100644 index 000000000000..ebf36caf706e --- /dev/null +++ b/crates/router/src/events/outgoing_webhook_logs.rs @@ -0,0 +1,110 @@ +use api_models::{enums::EventType as OutgoingWebhookEventType, webhooks::OutgoingWebhookContent}; +use serde::Serialize; +use serde_json::Value; +use time::OffsetDateTime; + +use super::{EventType, RawEvent}; + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct OutgoingWebhookEvent { + merchant_id: String, + event_id: String, + event_type: OutgoingWebhookEventType, + #[serde(flatten)] + content: Option, + is_error: bool, + error: Option, + created_at_timestamp: i128, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(tag = "outgoing_webhook_event_type", rename_all = "snake_case")] +pub enum OutgoingWebhookEventContent { + Payment { + payment_id: Option, + content: Value, + }, + Refund { + payment_id: String, + refund_id: String, + content: Value, + }, + Dispute { + payment_id: String, + attempt_id: String, + dispute_id: String, + content: Value, + }, + Mandate { + payment_method_id: String, + mandate_id: String, + content: Value, + }, +} +pub trait OutgoingWebhookEventMetric { + fn get_outgoing_webhook_event_type(&self) -> Option; +} +impl OutgoingWebhookEventMetric for OutgoingWebhookContent { + fn get_outgoing_webhook_event_type(&self) -> Option { + match self { + Self::PaymentDetails(payment_payload) => Some(OutgoingWebhookEventContent::Payment { + payment_id: payment_payload.payment_id.clone(), + content: masking::masked_serialize(&payment_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::RefundDetails(refund_payload) => Some(OutgoingWebhookEventContent::Refund { + payment_id: refund_payload.payment_id.clone(), + refund_id: refund_payload.refund_id.clone(), + content: masking::masked_serialize(&refund_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::DisputeDetails(dispute_payload) => Some(OutgoingWebhookEventContent::Dispute { + payment_id: dispute_payload.payment_id.clone(), + attempt_id: dispute_payload.attempt_id.clone(), + dispute_id: dispute_payload.dispute_id.clone(), + content: masking::masked_serialize(&dispute_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + Self::MandateDetails(mandate_payload) => Some(OutgoingWebhookEventContent::Mandate { + payment_method_id: mandate_payload.payment_method_id.clone(), + mandate_id: mandate_payload.mandate_id.clone(), + content: masking::masked_serialize(&mandate_payload) + .unwrap_or(serde_json::json!({"error":"failed to serialize"})), + }), + } + } +} + +impl OutgoingWebhookEvent { + pub fn new( + merchant_id: String, + event_id: String, + event_type: OutgoingWebhookEventType, + content: Option, + is_error: bool, + error: Option, + ) -> Self { + Self { + merchant_id, + event_id, + event_type, + content, + is_error, + error, + created_at_timestamp: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, + } + } +} + +impl TryFrom for RawEvent { + type Error = serde_json::Error; + + fn try_from(value: OutgoingWebhookEvent) -> Result { + Ok(Self { + event_type: EventType::OutgoingWebhookLogs, + key: value.merchant_id.clone(), + payload: serde_json::to_value(value)?, + }) + } +} diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs index 4c65b4677872..5a6d7043e6d0 100644 --- a/crates/router/src/services/kafka.rs +++ b/crates/router/src/services/kafka.rs @@ -84,6 +84,7 @@ pub struct KafkaSettings { refund_analytics_topic: String, api_logs_topic: String, connector_logs_topic: String, + outgoing_webhook_logs_topic: String, } impl KafkaSettings { @@ -140,6 +141,7 @@ pub struct KafkaProducer { refund_analytics_topic: String, api_logs_topic: String, connector_logs_topic: String, + outgoing_webhook_logs_topic: String, } struct RdKafkaProducer(ThreadedProducer); @@ -177,6 +179,7 @@ impl KafkaProducer { refund_analytics_topic: conf.refund_analytics_topic.clone(), api_logs_topic: conf.api_logs_topic.clone(), connector_logs_topic: conf.connector_logs_topic.clone(), + outgoing_webhook_logs_topic: conf.outgoing_webhook_logs_topic.clone(), }) } @@ -309,6 +312,7 @@ impl KafkaProducer { EventType::PaymentIntent => &self.intent_analytics_topic, EventType::Refund => &self.refund_analytics_topic, EventType::ConnectorApiLogs => &self.connector_logs_topic, + EventType::OutgoingWebhookLogs => &self.outgoing_webhook_logs_topic, } } } From f30ba89884d3abf2356cf1870d833a97d2411f69 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:21:40 +0530 Subject: [PATCH 272/443] feat: add deep health check (#3210) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/health_check.rs | 6 + crates/api_models/src/lib.rs | 1 + crates/router/src/consts.rs | 2 + crates/router/src/db.rs | 2 + crates/router/src/db/health_check.rs | 147 +++++++++++++++++++++++ crates/router/src/db/kafka_store.rs | 23 ++++ crates/router/src/routes/app.rs | 5 +- crates/router/src/routes/health.rs | 62 +++++++++- crates/router/src/services/api.rs | 8 ++ crates/router/src/services/api/client.rs | 2 + crates/storage_impl/src/errors.rs | 50 ++++++++ 11 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 crates/api_models/src/health_check.rs create mode 100644 crates/router/src/db/health_check.rs diff --git a/crates/api_models/src/health_check.rs b/crates/api_models/src/health_check.rs new file mode 100644 index 000000000000..d7bb120d0176 --- /dev/null +++ b/crates/api_models/src/health_check.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RouterHealthCheckResponse { + pub database: String, + pub redis: String, + pub locker: String, +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 935944cf74c2..459443747e36 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -16,6 +16,7 @@ pub mod errors; pub mod events; pub mod files; pub mod gsm; +pub mod health_check; pub mod locker_migration; pub mod mandates; pub mod organization; diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 4a2d2831d103..eff42c0cd7c4 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -70,3 +70,5 @@ pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant"; + +pub const LOCKER_HEALTH_CALL_PATH: &str = "/health"; diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 0cd4cb218810..5beace9cbb83 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -14,6 +14,7 @@ pub mod events; pub mod file; pub mod fraud_check; pub mod gsm; +pub mod health_check; mod kafka_store; pub mod locker_mock_up; pub mod mandate; @@ -103,6 +104,7 @@ pub trait StorageInterface: + user_role::UserRoleInterface + authorization::AuthorizationInterface + user::sample_data::BatchSampleDataInterface + + health_check::HealthCheckInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/health_check.rs b/crates/router/src/db/health_check.rs new file mode 100644 index 000000000000..73bc2a4321d7 --- /dev/null +++ b/crates/router/src/db/health_check.rs @@ -0,0 +1,147 @@ +use async_bb8_diesel::{AsyncConnection, AsyncRunQueryDsl}; +use diesel_models::ConfigNew; +use error_stack::ResultExt; +use router_env::logger; + +use super::{MockDb, StorageInterface, Store}; +use crate::{ + connection, + consts::LOCKER_HEALTH_CALL_PATH, + core::errors::{self, CustomResult}, + routes, + services::api as services, + types::storage, +}; + +#[async_trait::async_trait] +pub trait HealthCheckInterface { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>; + async fn health_check_redis( + &self, + db: &dyn StorageInterface, + ) -> CustomResult<(), errors::HealthCheckRedisError>; + async fn health_check_locker( + &self, + state: &routes::AppState, + ) -> CustomResult<(), errors::HealthCheckLockerError>; +} + +#[async_trait::async_trait] +impl HealthCheckInterface for Store { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + let conn = connection::pg_connection_write(self) + .await + .change_context(errors::HealthCheckDBError::DBError)?; + + let _data = conn + .transaction_async(|conn| { + Box::pin(async move { + let query = + diesel::select(diesel::dsl::sql::("1 + 1")); + let _x: i32 = query.get_result_async(&conn).await.map_err(|err| { + logger::error!(read_err=?err,"Error while reading element in the database"); + errors::HealthCheckDBError::DBReadError + })?; + + logger::debug!("Database read was successful"); + + let config = ConfigNew { + key: "test_key".to_string(), + config: "test_value".to_string(), + }; + + config.insert(&conn).await.map_err(|err| { + logger::error!(write_err=?err,"Error while writing to database"); + errors::HealthCheckDBError::DBWriteError + })?; + + logger::debug!("Database write was successful"); + + storage::Config::delete_by_key(&conn, "test_key").await.map_err(|err| { + logger::error!(delete_err=?err,"Error while deleting element in the database"); + errors::HealthCheckDBError::DBDeleteError + })?; + + logger::debug!("Database delete was successful"); + + Ok::<_, errors::HealthCheckDBError>(()) + }) + }) + .await?; + + Ok(()) + } + + async fn health_check_redis( + &self, + db: &dyn StorageInterface, + ) -> CustomResult<(), errors::HealthCheckRedisError> { + let redis_conn = db + .get_redis_conn() + .change_context(errors::HealthCheckRedisError::RedisConnectionError)?; + + redis_conn + .serialize_and_set_key_with_expiry("test_key", "test_value", 30) + .await + .change_context(errors::HealthCheckRedisError::SetFailed)?; + + logger::debug!("Redis set_key was successful"); + + redis_conn + .get_key("test_key") + .await + .change_context(errors::HealthCheckRedisError::GetFailed)?; + + logger::debug!("Redis get_key was successful"); + + redis_conn + .delete_key("test_key") + .await + .change_context(errors::HealthCheckRedisError::DeleteFailed)?; + + logger::debug!("Redis delete_key was successful"); + + Ok(()) + } + + async fn health_check_locker( + &self, + state: &routes::AppState, + ) -> CustomResult<(), errors::HealthCheckLockerError> { + let locker = &state.conf.locker; + if !locker.mock_locker { + let mut url = locker.host_rs.to_owned(); + url.push_str(LOCKER_HEALTH_CALL_PATH); + let request = services::Request::new(services::Method::Get, &url); + services::call_connector_api(state, request) + .await + .change_context(errors::HealthCheckLockerError::FailedToCallLocker)? + .ok(); + } + + logger::debug!("Locker call was successful"); + + Ok(()) + } +} + +#[async_trait::async_trait] +impl HealthCheckInterface for MockDb { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + Ok(()) + } + + async fn health_check_redis( + &self, + _: &dyn StorageInterface, + ) -> CustomResult<(), errors::HealthCheckRedisError> { + Ok(()) + } + + async fn health_check_locker( + &self, + _: &routes::AppState, + ) -> CustomResult<(), errors::HealthCheckLockerError> { + Ok(()) + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index db94c1bcbca9..1184992a8f7c 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -43,6 +43,7 @@ use crate::{ events::EventInterface, file::FileMetadataInterface, gsm::GsmInterface, + health_check::HealthCheckInterface, locker_mock_up::LockerMockUpInterface, mandate::MandateInterface, merchant_account::MerchantAccountInterface, @@ -57,6 +58,7 @@ use crate::{ routing_algorithm::RoutingAlgorithmInterface, MasterKeyInterface, StorageInterface, }, + routes, services::{authentication, kafka::KafkaProducer, Store}, types::{ domain, @@ -2131,3 +2133,24 @@ impl AuthorizationInterface for KafkaStore { .await } } + +#[async_trait::async_trait] +impl HealthCheckInterface for KafkaStore { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + self.diesel_store.health_check_db().await + } + + async fn health_check_redis( + &self, + db: &dyn StorageInterface, + ) -> CustomResult<(), errors::HealthCheckRedisError> { + self.diesel_store.health_check_redis(db).await + } + + async fn health_check_locker( + &self, + state: &routes::AppState, + ) -> CustomResult<(), errors::HealthCheckLockerError> { + self.diesel_store.health_check_locker(state).await + } +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0357cedd443c..6625a206be21 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -253,9 +253,10 @@ pub struct Health; impl Health { pub fn server(state: AppState) -> Scope { - web::scope("") + web::scope("health") .app_data(web::Data::new(state)) - .service(web::resource("/health").route(web::get().to(health))) + .service(web::resource("").route(web::get().to(health))) + .service(web::resource("/deep_check").route(web::post().to(deep_health_check))) } } diff --git a/crates/router/src/routes/health.rs b/crates/router/src/routes/health.rs index 7c7f29bd1819..f07b744f7f52 100644 --- a/crates/router/src/routes/health.rs +++ b/crates/router/src/routes/health.rs @@ -1,7 +1,9 @@ +use actix_web::web; +use api_models::health_check::RouterHealthCheckResponse; use router_env::{instrument, logger, tracing}; -use crate::routes::metrics; - +use super::app; +use crate::{routes::metrics, services}; /// . // #[logger::instrument(skip_all, name = "name1", level = "warn", fields( key1 = "val1" ))] #[instrument(skip_all)] @@ -11,3 +13,59 @@ pub async fn health() -> impl actix_web::Responder { logger::info!("Health was called"); actix_web::HttpResponse::Ok().body("health is good") } + +#[instrument(skip_all)] +pub async fn deep_health_check(state: web::Data) -> impl actix_web::Responder { + metrics::HEALTH_METRIC.add(&metrics::CONTEXT, 1, &[]); + let db = &*state.store; + let mut status_code = 200; + logger::info!("Deep health check was called"); + + logger::debug!("Database health check begin"); + + let db_status = match db.health_check_db().await { + Ok(_) => "Health is good".to_string(), + Err(err) => { + status_code = 500; + err.to_string() + } + }; + logger::debug!("Database health check end"); + + logger::debug!("Redis health check begin"); + + let redis_status = match db.health_check_redis(db).await { + Ok(_) => "Health is good".to_string(), + Err(err) => { + status_code = 500; + err.to_string() + } + }; + + logger::debug!("Redis health check end"); + + logger::debug!("Locker health check begin"); + + let locker_status = match db.health_check_locker(&state).await { + Ok(_) => "Health is good".to_string(), + Err(err) => { + status_code = 500; + err.to_string() + } + }; + + logger::debug!("Locker health check end"); + + let response = serde_json::to_string(&RouterHealthCheckResponse { + database: db_status, + redis: redis_status, + locker: locker_status, + }) + .unwrap_or_default(); + + if status_code == 200 { + services::http_response_json(response) + } else { + services::http_server_error_json_response(response) + } +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 92fda578727c..71687e02c121 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1138,6 +1138,14 @@ pub fn http_response_json(response: T) -> HttpRe .body(response) } +pub fn http_server_error_json_response( + response: T, +) -> HttpResponse { + HttpResponse::InternalServerError() + .content_type(mime::APPLICATION_JSON) + .body(response) +} + pub fn http_response_json_with_headers( response: T, mut headers: Vec<(String, String)>, diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index cc7353dcda6b..fca85c41699a 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -10,6 +10,7 @@ use router_env::tracing_actix_web::RequestId; use super::{request::Maskable, Request}; use crate::{ configs::settings::{Locker, Proxy}, + consts::LOCKER_HEALTH_CALL_PATH, core::{ errors::{ApiClientError, CustomResult}, payments, @@ -119,6 +120,7 @@ pub fn proxy_bypass_urls(locker: &Locker) -> Vec { format!("{locker_host_rs}/cards/add"), format!("{locker_host_rs}/cards/retrieve"), format!("{locker_host_rs}/cards/delete"), + format!("{locker_host_rs}{}", LOCKER_HEALTH_CALL_PATH), format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index f0cbebf78c55..50173bb1c739 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -376,3 +376,53 @@ pub enum ConnectorError { #[error("Missing 3DS redirection payload: {field_name}")] MissingConnectorRedirectionPayload { field_name: &'static str }, } + +#[derive(Debug, thiserror::Error)] +pub enum HealthCheckDBError { + #[error("Error while connecting to database")] + DBError, + #[error("Error while writing to database")] + DBWriteError, + #[error("Error while reading element in the database")] + DBReadError, + #[error("Error while deleting element in the database")] + DBDeleteError, + #[error("Unpredictable error occurred")] + UnknownError, + #[error("Error in database transaction")] + TransactionError, +} + +impl From for HealthCheckDBError { + fn from(error: diesel::result::Error) -> Self { + match error { + diesel::result::Error::DatabaseError(_, _) => Self::DBError, + + diesel::result::Error::RollbackErrorOnCommit { .. } + | diesel::result::Error::RollbackTransaction + | diesel::result::Error::AlreadyInTransaction + | diesel::result::Error::NotInTransaction + | diesel::result::Error::BrokenTransactionManager => Self::TransactionError, + + _ => Self::UnknownError, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum HealthCheckRedisError { + #[error("Failed to establish Redis connection")] + RedisConnectionError, + #[error("Failed to set key value in Redis")] + SetFailed, + #[error("Failed to get key value in Redis")] + GetFailed, + #[error("Failed to delete key value in Redis")] + DeleteFailed, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum HealthCheckLockerError { + #[error("Failed to establish Locker connection")] + FailedToCallLocker, +} From c8279b110e6c55784f042aebb956931e1870b0ca Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:22:31 +0530 Subject: [PATCH 273/443] chore: address Rust 1.75 clippy lints (#3231) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/common_utils/src/request.rs | 3 +- crates/kgraph_utils/src/mca.rs | 2 +- crates/masking/src/diesel.rs | 1 - crates/masking/src/serde.rs | 2 +- crates/redis_interface/src/lib.rs | 2 +- crates/router/src/configs/settings.rs | 2 +- .../braintree_graphql_transformers.rs | 4 +- .../src/connector/checkout/transformers.rs | 5 +- .../src/connector/stripe/transformers.rs | 2 +- crates/router/src/connector/utils.rs | 160 +++++++++--------- crates/router/src/connector/wise.rs | 4 +- crates/router/src/core/fraud_check.rs | 10 +- crates/router/src/core/payments.rs | 5 +- .../router/src/core/payments/transformers.rs | 2 +- crates/router/src/db/dispute.rs | 4 +- crates/router/src/macros.rs | 5 +- crates/router/src/services/api.rs | 28 +-- .../types/api/connector_onboarding/paypal.rs | 2 +- crates/router/src/types/storage.rs | 1 - crates/router/src/types/storage/query.rs | 1 - crates/router/src/utils.rs | 20 +-- crates/router/src/utils/user/sample_data.rs | 4 +- crates/router/tests/connectors/utils.rs | 2 +- crates/scheduler/src/consumer.rs | 6 + crates/scheduler/src/producer.rs | 6 + 25 files changed, 141 insertions(+), 142 deletions(-) delete mode 100644 crates/router/src/types/storage/query.rs diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index e3aecb4b5695..f063a1940bef 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -138,8 +138,7 @@ impl RequestBuilder { } pub fn headers(mut self, headers: Vec<(String, Maskable)>) -> Self { - let mut h = headers.into_iter().map(|(h, v)| (h, v)); - self.headers.extend(&mut h); + self.headers.extend(headers); self } diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index 0e224a8f3d9d..a04e052514d6 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -156,7 +156,7 @@ fn compile_request_pm_types( let or_node_neighbor_id = if amount_nodes.len() == 1 { amount_nodes - .get(0) + .first() .copied() .ok_or(KgraphError::IndexingError)? } else { diff --git a/crates/masking/src/diesel.rs b/crates/masking/src/diesel.rs index 307f083d27fb..f3576298bdb1 100644 --- a/crates/masking/src/diesel.rs +++ b/crates/masking/src/diesel.rs @@ -2,7 +2,6 @@ //! Diesel-related. //! -pub use diesel::Expression; use diesel::{ backend::Backend, deserialize::{self, FromSql, Queryable}, diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs index 944392e693ff..bb81717fd670 100644 --- a/crates/masking/src/serde.rs +++ b/crates/masking/src/serde.rs @@ -3,7 +3,7 @@ //! pub use erased_serde::Serialize as ErasedSerialize; -pub use serde::{de, ser, Deserialize, Serialize, Serializer}; +pub use serde::{de, Deserialize, Serialize, Serializer}; use serde_json::{value::Serializer as JsonValueSerializer, Value}; use crate::{Secret, Strategy, StrongSecret, ZeroizableSecret}; diff --git a/crates/redis_interface/src/lib.rs b/crates/redis_interface/src/lib.rs index 7111869a5c03..33d40ebe155d 100644 --- a/crates/redis_interface/src/lib.rs +++ b/crates/redis_interface/src/lib.rs @@ -28,7 +28,7 @@ pub use fred::interfaces::PubsubInterface; use fred::{interfaces::ClientLike, prelude::EventInterface}; use router_env::logger; -pub use self::{commands::*, types::*}; +pub use self::types::*; pub struct RedisConnectionPool { pub pool: fred::prelude::RedisPool, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index b62831950856..db59d7f29148 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -556,7 +556,7 @@ impl From for storage_impl::config::Database { dbname: val.dbname, pool_size: val.pool_size, connection_timeout: val.connection_timeout, - queue_strategy: val.queue_strategy.into(), + queue_strategy: val.queue_strategy, min_idle: val.min_idle, max_lifetime: val.max_lifetime, } diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index 9bdbd4392f7c..e0baf034f72a 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -297,11 +297,11 @@ fn build_error_response( get_error_response( response - .get(0) + .first() .and_then(|err_details| err_details.extensions.as_ref()) .and_then(|extensions| extensions.legacy_code.clone()), response - .get(0) + .first() .map(|err_details| err_details.message.clone()), reason, http_code, diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 5bd80a10c4b5..6e7656c38a6f 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -1026,10 +1026,7 @@ impl utils::MultipleCaptureSyncResponse for Box { self.status == CheckoutPaymentStatus::Captured } fn get_amount_captured(&self) -> Option { - match self.amount { - Some(amount) => amount.try_into().ok(), - None => None, - } + self.amount.map(Into::into) } } diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index b044331379a1..ba5dfc7fef91 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -2336,7 +2336,7 @@ pub fn get_connector_metadata( let next_action_response = next_action .and_then(|next_action_response| match next_action_response { StripeNextActionResponse::DisplayBankTransferInstructions(response) => { - let bank_instructions = response.financial_addresses.get(0); + let bank_instructions = response.financial_addresses.first(); let (sepa_bank_instructions, bacs_bank_instructions) = bank_instructions.map_or((None, None), |financial_address| { ( diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 24def6253726..0dca3ace9479 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1512,86 +1512,6 @@ pub fn get_error_code_error_message_based_on_priority( .cloned() } -#[cfg(test)] -mod error_code_error_message_tests { - #![allow(clippy::unwrap_used)] - use super::*; - - struct TestConnector; - - impl ConnectorErrorTypeMapping for TestConnector { - fn get_connector_error_type( - &self, - error_code: String, - error_message: String, - ) -> ConnectorErrorType { - match (error_code.as_str(), error_message.as_str()) { - ("01", "INVALID_MERCHANT") => ConnectorErrorType::BusinessError, - ("03", "INVALID_CVV") => ConnectorErrorType::UserError, - ("04", "04") => ConnectorErrorType::TechnicalError, - _ => ConnectorErrorType::UnknownError, - } - } - } - - #[test] - fn test_get_error_code_error_message_based_on_priority() { - let error_code_message_list_unknown = vec![ - ErrorCodeAndMessage { - error_code: "01".to_string(), - error_message: "INVALID_MERCHANT".to_string(), - }, - ErrorCodeAndMessage { - error_code: "05".to_string(), - error_message: "05".to_string(), - }, - ErrorCodeAndMessage { - error_code: "03".to_string(), - error_message: "INVALID_CVV".to_string(), - }, - ErrorCodeAndMessage { - error_code: "04".to_string(), - error_message: "04".to_string(), - }, - ]; - let error_code_message_list_user = vec![ - ErrorCodeAndMessage { - error_code: "01".to_string(), - error_message: "INVALID_MERCHANT".to_string(), - }, - ErrorCodeAndMessage { - error_code: "03".to_string(), - error_message: "INVALID_CVV".to_string(), - }, - ]; - let error_code_error_message_unknown = get_error_code_error_message_based_on_priority( - TestConnector, - error_code_message_list_unknown, - ); - let error_code_error_message_user = get_error_code_error_message_based_on_priority( - TestConnector, - error_code_message_list_user, - ); - let error_code_error_message_none = - get_error_code_error_message_based_on_priority(TestConnector, vec![]); - assert_eq!( - error_code_error_message_unknown, - Some(ErrorCodeAndMessage { - error_code: "05".to_string(), - error_message: "05".to_string(), - }) - ); - assert_eq!( - error_code_error_message_user, - Some(ErrorCodeAndMessage { - error_code: "03".to_string(), - error_message: "INVALID_CVV".to_string(), - }) - ); - assert_eq!(error_code_error_message_none, None); - } -} - pub trait MultipleCaptureSyncResponse { fn get_connector_capture_id(&self) -> String; fn get_capture_attempt_status(&self) -> enums::AttemptStatus; @@ -1785,3 +1705,83 @@ impl FrmTransactionRouterDataRequest for fraud_check::FrmTransactionRouterData { } } } + +#[cfg(test)] +mod error_code_error_message_tests { + #![allow(clippy::unwrap_used)] + use super::*; + + struct TestConnector; + + impl ConnectorErrorTypeMapping for TestConnector { + fn get_connector_error_type( + &self, + error_code: String, + error_message: String, + ) -> ConnectorErrorType { + match (error_code.as_str(), error_message.as_str()) { + ("01", "INVALID_MERCHANT") => ConnectorErrorType::BusinessError, + ("03", "INVALID_CVV") => ConnectorErrorType::UserError, + ("04", "04") => ConnectorErrorType::TechnicalError, + _ => ConnectorErrorType::UnknownError, + } + } + } + + #[test] + fn test_get_error_code_error_message_based_on_priority() { + let error_code_message_list_unknown = vec![ + ErrorCodeAndMessage { + error_code: "01".to_string(), + error_message: "INVALID_MERCHANT".to_string(), + }, + ErrorCodeAndMessage { + error_code: "05".to_string(), + error_message: "05".to_string(), + }, + ErrorCodeAndMessage { + error_code: "03".to_string(), + error_message: "INVALID_CVV".to_string(), + }, + ErrorCodeAndMessage { + error_code: "04".to_string(), + error_message: "04".to_string(), + }, + ]; + let error_code_message_list_user = vec![ + ErrorCodeAndMessage { + error_code: "01".to_string(), + error_message: "INVALID_MERCHANT".to_string(), + }, + ErrorCodeAndMessage { + error_code: "03".to_string(), + error_message: "INVALID_CVV".to_string(), + }, + ]; + let error_code_error_message_unknown = get_error_code_error_message_based_on_priority( + TestConnector, + error_code_message_list_unknown, + ); + let error_code_error_message_user = get_error_code_error_message_based_on_priority( + TestConnector, + error_code_message_list_user, + ); + let error_code_error_message_none = + get_error_code_error_message_based_on_priority(TestConnector, vec![]); + assert_eq!( + error_code_error_message_unknown, + Some(ErrorCodeAndMessage { + error_code: "05".to_string(), + error_message: "05".to_string(), + }) + ); + assert_eq!( + error_code_error_message_user, + Some(ErrorCodeAndMessage { + error_code: "03".to_string(), + error_message: "INVALID_CVV".to_string(), + }) + ); + assert_eq!(error_code_error_message_none, None); + } +} diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index 0674cc2ff8bc..d2ec08c607c4 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -90,7 +90,7 @@ impl ConnectorCommon for Wise { let default_status = response.status.unwrap_or_default().to_string(); match response.errors { Some(errs) => { - if let Some(e) = errs.get(0) { + if let Some(e) = errs.first() { Ok(types::ErrorResponse { status_code: res.status_code, code: e.code.clone(), @@ -301,7 +301,7 @@ impl services::ConnectorIntegration = list.into_iter().map(ForeignFrom::foreign_from).collect(); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 577480d311d7..ff8b95d4ab44 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -547,7 +547,7 @@ where if third_party_sdk_session_next_action(&payment_attempt, operation) { next_action_response = Some( api_models::payments::NextActionData::ThirdPartySdkSessionToken { - session_token: payment_data.sessions_token.get(0).cloned(), + session_token: payment_data.sessions_token.first().cloned(), }, ) } diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index c63585205bb3..4529b121fa24 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -572,7 +572,7 @@ mod tests { assert_eq!(1, found_disputes.len()); - assert_eq!(created_dispute, found_disputes.get(0).unwrap().clone()); + assert_eq!(created_dispute, found_disputes.first().unwrap().clone()); } #[tokio::test] @@ -611,7 +611,7 @@ mod tests { assert_eq!(1, found_disputes.len()); - assert_eq!(created_dispute, found_disputes.get(0).unwrap().clone()); + assert_eq!(created_dispute, found_disputes.first().unwrap().clone()); } mod update_dispute { diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index e6c9dba7d6e2..efe71e49bb04 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,4 +1 @@ -pub use common_utils::{ - async_spawn, collect_missing_value_keys, fallback_reverse_lookup_not_found, newtype, - newtype_impl, -}; +pub use common_utils::{collect_missing_value_keys, newtype}; diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 71687e02c121..9ac8d5e5eb41 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1539,16 +1539,16 @@ pub fn build_redirection_form( var responseForm = document.createElement('form'); responseForm.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/nmi\"); responseForm.method='POST'; - + const threeDSsecureInterface = threeDS.createUI(options); threeDSsecureInterface.start('body'); - + threeDSsecureInterface.on('challenge', function(e) {{ console.log('Challenged'); }}); - + threeDSsecureInterface.on('complete', function(e) {{ - + var item1=document.createElement('input'); item1.type='hidden'; item1.name='cavv'; @@ -1582,11 +1582,11 @@ pub fn build_redirection_form( document.body.appendChild(responseForm); responseForm.submit(); }}); - + threeDSsecureInterface.on('failure', function(e) {{ responseForm.submit(); }}); - + " ))) } @@ -1594,14 +1594,6 @@ pub fn build_redirection_form( } } -#[cfg(test)] -mod tests { - #[test] - fn test_mime_essence() { - assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); - } -} - pub fn build_payment_link_html( payment_link_data: PaymentLinkFormData, ) -> CustomResult { @@ -1631,3 +1623,11 @@ pub fn build_payment_link_html( fn get_hyper_loader_sdk(sdk_url: &str) -> String { format!("") } + +#[cfg(test)] +mod tests { + #[test] + fn test_mime_essence() { + assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); + } +} diff --git a/crates/router/src/types/api/connector_onboarding/paypal.rs b/crates/router/src/types/api/connector_onboarding/paypal.rs index 0cc026d4d7ad..dbfdd6f50075 100644 --- a/crates/router/src/types/api/connector_onboarding/paypal.rs +++ b/crates/router/src/types/api/connector_onboarding/paypal.rs @@ -177,7 +177,7 @@ pub enum VettingStatus { impl SellerStatusResponse { pub fn extract_merchant_details_url(self, paypal_base_url: &str) -> RouterResult { self.links - .get(0) + .first() .and_then(|link| link.href.strip_prefix('/')) .map(|link| format!("{}{}", paypal_base_url, link)) .ok_or(ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 1dc241cde20c..56d3272b9471 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -26,7 +26,6 @@ pub mod payment_link; pub mod payment_method; pub mod payout_attempt; pub mod payouts; -mod query; pub mod refund; pub mod reverse_lookup; pub mod routing_algorithm; diff --git a/crates/router/src/types/storage/query.rs b/crates/router/src/types/storage/query.rs deleted file mode 100644 index 1483dec3eab3..000000000000 --- a/crates/router/src/types/storage/query.rs +++ /dev/null @@ -1 +0,0 @@ -pub use diesel_models::query::*; diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 42116e1ecbf0..aaa145099e4e 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -196,16 +196,6 @@ impl QrImage { } } -#[cfg(test)] -mod tests { - use crate::utils; - #[test] - fn test_image_data_source_url() { - let qr_image_data_source_url = utils::QrImage::new_from_data("Hyperswitch".to_string()); - assert!(qr_image_data_source_url.is_ok()); - } -} - pub async fn find_payment_intent_from_payment_id_type( db: &dyn StorageInterface, payment_id_type: payments::PaymentIdType, @@ -804,3 +794,13 @@ pub async fn flatten_join_error(handle: Handle) -> RouterResult { .attach_printable("Join Error"), } } + +#[cfg(test)] +mod tests { + use crate::utils; + #[test] + fn test_image_data_source_url() { + let qr_image_data_source_url = utils::QrImage::new_from_data("Hyperswitch".to_string()); + assert!(qr_image_data_source_url.is_ok()); + } +} diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 9f95e2d078dd..5ce0818b82b3 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -48,9 +48,9 @@ pub async fn generate_sample_data( .change_context(SampleDataError::InternalServerError) .attach_printable("Error while parsing primary business details")?; - let business_country_default = merchant_parsed_details.get(0).map(|x| x.country); + let business_country_default = merchant_parsed_details.first().map(|x| x.country); - let business_label_default = merchant_parsed_details.get(0).map(|x| x.business.clone()); + let business_label_default = merchant_parsed_details.first().map(|x| x.business.clone()); let profile_id = crate::core::utils::get_profile_id_from_business_details( business_country_default, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 62fce84f1f9d..c27a648a3051 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -458,7 +458,7 @@ pub trait ConnectorActions: Connector { customer_details: Some(payments::CustomerDetails { customer_id: core_utils::get_or_generate_id("customer_id", &None, "cust_").ok(), name: Some(Secret::new("John Doe".to_string())), - email: TryFrom::try_from(Secret::new("john.doe@example".to_string())).ok(), + email: TryFrom::try_from("john.doe@example".to_string()).ok(), phone: Some(Secret::new("620874518".to_string())), phone_country_code: Some("+31".to_string()), }), diff --git a/crates/scheduler/src/consumer.rs b/crates/scheduler/src/consumer.rs index ef0386bec299..ccc943afba3c 100644 --- a/crates/scheduler/src/consumer.rs +++ b/crates/scheduler/src/consumer.rs @@ -40,9 +40,15 @@ pub async fn start_consumer( use rand::distributions::{Distribution, Uniform}; let mut rng = rand::thread_rng(); + + // TODO: this can be removed once rand-0.9 is released + // reference - https://github.com/rust-random/rand/issues/1326#issuecomment-1635331942 + #[allow(unknown_lints)] + #[allow(clippy::unnecessary_fallible_conversions)] let timeout = Uniform::try_from(0..=settings.loop_interval) .into_report() .change_context(errors::ProcessTrackerError::ConfigurationError)?; + tokio::time::sleep(Duration::from_millis(timeout.sample(&mut rng))).await; let mut interval = tokio::time::interval(Duration::from_millis(settings.loop_interval)); diff --git a/crates/scheduler/src/producer.rs b/crates/scheduler/src/producer.rs index 52510e1842e0..bcf37cdf6f22 100644 --- a/crates/scheduler/src/producer.rs +++ b/crates/scheduler/src/producer.rs @@ -30,9 +30,15 @@ where use rand::distributions::{Distribution, Uniform}; let mut rng = rand::thread_rng(); + + // TODO: this can be removed once rand-0.9 is released + // reference - https://github.com/rust-random/rand/issues/1326#issuecomment-1635331942 + #[allow(unknown_lints)] + #[allow(clippy::unnecessary_fallible_conversions)] let timeout = Uniform::try_from(0..=scheduler_settings.loop_interval) .into_report() .change_context(errors::ProcessTrackerError::ConfigurationError)?; + tokio::time::sleep(Duration::from_millis(timeout.sample(&mut rng))).await; let mut interval = tokio::time::interval(std::time::Duration::from_millis( From 252443a50dc48939eb08b3bcd67273bb71bbe349 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Fri, 5 Jan 2024 17:32:36 +0530 Subject: [PATCH 274/443] feat: include version number in response headers and on application startup (#3045) --- crates/drainer/src/main.rs | 3 +++ crates/router/Cargo.toml | 2 +- crates/router/src/bin/router.rs | 3 +++ crates/router/src/bin/scheduler.rs | 8 ++++++-- crates/router/src/middleware.rs | 8 +++++++- crates/router_env/src/env.rs | 10 ++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/drainer/src/main.rs b/crates/drainer/src/main.rs index 2b4142abc0c7..9e8b8e275cbd 100644 --- a/crates/drainer/src/main.rs +++ b/crates/drainer/src/main.rs @@ -20,6 +20,9 @@ async fn main() -> DrainerResult<()> { let shutdown_intervals = conf.drainer.shutdown_interval; let loop_interval = conf.drainer.loop_interval; + #[cfg(feature = "vergen")] + println!("Starting drainer (Version: {})", router_env::git_tag!()); + let _guard = router_env::setup( &conf.log, router_env::service_name!(), diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index a1ddc37bbf2c..eb7fbc7ddbc9 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -16,7 +16,7 @@ email = ["external_services/email", "dep:aws-config", "olap"] frm = [] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing"] +release = ["kms", "stripe", "basilisk", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] diff --git a/crates/router/src/bin/router.rs b/crates/router/src/bin/router.rs index beb2869f998c..a02758d8edd5 100644 --- a/crates/router/src/bin/router.rs +++ b/crates/router/src/bin/router.rs @@ -34,6 +34,9 @@ async fn main() -> ApplicationResult<()> { conf.validate() .expect("Failed to validate router configuration"); + #[cfg(feature = "vergen")] + println!("Starting router (Version: {})", router_env::git_tag!()); + let _guard = router_env::setup( &conf.log, router_env::service_name!(), diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 32e9cfc6ca29..b800ecb897e5 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -22,8 +22,6 @@ use tokio::sync::{mpsc, oneshot}; const SCHEDULER_FLOW: &str = "SCHEDULER_FLOW"; #[tokio::main] async fn main() -> CustomResult<(), ProcessTrackerError> { - // console_subscriber::init(); - let cmd_line = ::parse(); #[allow(clippy::expect_used)] @@ -58,6 +56,12 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { let scheduler_flow = scheduler::SchedulerFlow::from_str(&scheduler_flow_str) .expect("Unable to parse SchedulerFlow from environment variable"); + #[cfg(feature = "vergen")] + println!( + "Starting {scheduler_flow} (Version: {})", + router_env::git_tag!() + ); + let _guard = router_env::setup( &state.conf.log, &scheduler_flow_str, diff --git a/crates/router/src/middleware.rs b/crates/router/src/middleware.rs index 0f2c5bd2cb7f..c6c94d3a78ea 100644 --- a/crates/router/src/middleware.rs +++ b/crates/router/src/middleware.rs @@ -70,7 +70,13 @@ where pub fn default_response_headers() -> actix_web::middleware::DefaultHeaders { use actix_web::http::header; - actix_web::middleware::DefaultHeaders::new() + let default_headers_middleware = actix_web::middleware::DefaultHeaders::new(); + + #[cfg(feature = "vergen")] + let default_headers_middleware = + default_headers_middleware.add(("x-hyperswitch-version", router_env::git_tag!())); + + default_headers_middleware // Max age of 1 year in seconds, equal to `60 * 60 * 24 * 365` seconds. .add((header::STRICT_TRANSPORT_SECURITY, "max-age=31536000")) .add((header::VIA, "HyperSwitch")) diff --git a/crates/router_env/src/env.rs b/crates/router_env/src/env.rs index e57c38e7e4ad..644f9b50a126 100644 --- a/crates/router_env/src/env.rs +++ b/crates/router_env/src/env.rs @@ -167,3 +167,13 @@ macro_rules! profile { env!("CARGO_PROFILE") }; } + +/// The latest git tag. If tags are not present in the repository, the short commit hash is used +/// instead. Refer to the [`git describe`](https://git-scm.com/docs/git-describe) documentation for +/// more details. +#[macro_export] +macro_rules! git_tag { + () => { + env!("VERGEN_GIT_DESCRIBE") + }; +} From c3172ef60603325a1d9e5cab45e72d23a383e218 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 5 Jan 2024 18:26:03 +0530 Subject: [PATCH 275/443] feat(merchant_account): Add list multiple merchants in `MerchantAccountInterface` (#3220) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/query/merchant_account.rs | 20 ++++++ .../src/query/merchant_key_store.rs | 20 ++++++ crates/router/src/db/kafka_store.rs | 21 ++++++ crates/router/src/db/merchant_account.rs | 68 +++++++++++++++++++ crates/router/src/db/merchant_key_store.rs | 56 +++++++++++++++ 5 files changed, 185 insertions(+) diff --git a/crates/diesel_models/src/query/merchant_account.rs b/crates/diesel_models/src/query/merchant_account.rs index ef9a4165d6f4..ba20f2e36607 100644 --- a/crates/diesel_models/src/query/merchant_account.rs +++ b/crates/diesel_models/src/query/merchant_account.rs @@ -110,4 +110,24 @@ impl MerchantAccount { ) .await } + + #[instrument(skip_all)] + pub async fn list_multiple_merchant_accounts( + conn: &PgPooledConn, + merchant_ids: Vec, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl::merchant_id.eq_any(merchant_ids), + None, + None, + None, + ) + .await + } } diff --git a/crates/diesel_models/src/query/merchant_key_store.rs b/crates/diesel_models/src/query/merchant_key_store.rs index 27ec3be9fcd0..0e2ec1ddadbd 100644 --- a/crates/diesel_models/src/query/merchant_key_store.rs +++ b/crates/diesel_models/src/query/merchant_key_store.rs @@ -39,4 +39,24 @@ impl MerchantKeyStore { ) .await } + + #[instrument(skip(conn))] + pub async fn list_multiple_key_stores( + conn: &PgPooledConn, + merchant_ids: Vec, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as diesel::Table>::PrimaryKey, + _, + >( + conn, + dsl::merchant_id.eq_any(merchant_ids), + None, + None, + None, + ) + .await + } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 1184992a8f7c..19a83088a06f 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -662,6 +662,16 @@ impl MerchantAccountInterface for KafkaStore { .delete_merchant_account_by_merchant_id(merchant_id) .await } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + merchant_ids: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_multiple_merchant_accounts(merchant_ids) + .await + } } #[async_trait::async_trait] @@ -1615,6 +1625,17 @@ impl MerchantKeyStoreInterface for KafkaStore { .delete_merchant_key_store_by_merchant_id(merchant_id) .await } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_multiple_key_stores(merchant_ids, key) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 0d3ce99b948d..70d417c0366d 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "olap")] +use std::collections::HashMap; + use common_utils::ext_traits::AsyncExt; use error_stack::{IntoReport, ResultExt}; #[cfg(feature = "accounts_cache")] @@ -65,6 +68,12 @@ where &self, merchant_id: &str, ) -> CustomResult; + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + merchant_ids: Vec, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -294,6 +303,57 @@ impl MerchantAccountInterface for Store { Ok(is_deleted) } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + merchant_ids: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + + let encrypted_merchant_accounts = + storage::MerchantAccount::list_multiple_merchant_accounts(&conn, merchant_ids) + .await + .map_err(Into::into) + .into_report()?; + + let db_master_key = self.get_master_key().to_vec().into(); + + let merchant_key_stores = self + .list_multiple_key_stores( + encrypted_merchant_accounts + .iter() + .map(|merchant_account| &merchant_account.merchant_id) + .cloned() + .collect(), + &db_master_key, + ) + .await?; + + let key_stores_by_id: HashMap<_, _> = merchant_key_stores + .iter() + .map(|key_store| (key_store.merchant_id.to_owned(), key_store)) + .collect(); + + let merchant_accounts = + futures::future::try_join_all(encrypted_merchant_accounts.into_iter().map( + |merchant_account| async { + let key_store = key_stores_by_id.get(&merchant_account.merchant_id).ok_or( + errors::StorageError::ValueNotFound(format!( + "merchant_key_store with merchant_id = {}", + merchant_account.merchant_id + )), + )?; + merchant_account + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }, + )) + .await?; + + Ok(merchant_accounts) + } } #[async_trait::async_trait] @@ -392,6 +452,14 @@ impl MerchantAccountInterface for MockDb { ) -> CustomResult, errors::StorageError> { Err(errors::StorageError::MockDbError)? } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + _merchant_ids: Vec, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } } #[cfg(feature = "accounts_cache")] diff --git a/crates/router/src/db/merchant_key_store.rs b/crates/router/src/db/merchant_key_store.rs index 970e2b770324..630b833afdde 100644 --- a/crates/router/src/db/merchant_key_store.rs +++ b/crates/router/src/db/merchant_key_store.rs @@ -32,6 +32,13 @@ pub trait MerchantKeyStoreInterface { &self, merchant_id: &str, ) -> CustomResult; + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -128,6 +135,33 @@ impl MerchantKeyStoreInterface for Store { .await } } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError> { + let fetch_func = || async { + let conn = connection::pg_connection_read(self).await?; + + diesel_models::merchant_key_store::MerchantKeyStore::list_multiple_key_stores( + &conn, + merchant_ids, + ) + .await + .map_err(Into::into) + .into_report() + }; + + futures::future::try_join_all(fetch_func().await?.into_iter().map(|key_store| async { + key_store + .convert(key) + .await + .change_context(errors::StorageError::DecryptionError) + })) + .await + } } #[async_trait::async_trait] @@ -194,6 +228,28 @@ impl MerchantKeyStoreInterface for MockDb { merchant_key_stores.remove(index); Ok(true) } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, errors::StorageError> { + let merchant_key_stores = self.merchant_key_store.lock().await; + futures::future::try_join_all( + merchant_key_stores + .iter() + .filter(|merchant_key| merchant_ids.contains(&merchant_key.merchant_id)) + .map(|merchant_key| async { + merchant_key + .to_owned() + .convert(key) + .await + .change_context(errors::StorageError::DecryptionError) + }), + ) + .await + } } #[cfg(test)] From 64babd34786ba8e6f63aa1dba1cbd1bc6264f2ac Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Fri, 5 Jan 2024 18:56:07 +0530 Subject: [PATCH 276/443] fix(connector): [NMI] Populating `ErrorResponse` with required fields and Mapping `connector_response_reference_id` (#3214) Co-authored-by: Prasunna Soppa --- crates/router/src/connector/nmi.rs | 8 +-- .../router/src/connector/nmi/transformers.rs | 53 ++++++++++--------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index 0c01c752039f..d514eefb10aa 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -75,10 +75,12 @@ impl ConnectorCommon for Nmi { .parse_struct("StandardResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; Ok(ErrorResponse { - message: response.responsetext, + message: response.responsetext.to_owned(), status_code: res.status_code, - reason: None, - ..Default::default() + reason: Some(response.responsetext), + code: response.response_code, + attempt_status: None, + connector_transaction_id: Some(response.transactionid), }) } } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index b9ad5b8e1883..677bf303d95f 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -143,6 +143,7 @@ pub struct NmiVaultResponse { pub responsetext: String, pub customer_vault_id: Option, pub response_code: String, + pub transactionid: String, } impl @@ -205,7 +206,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.transactionid), incremental_authorization_allowed: None, }), enums::AttemptStatus::AuthenticationPending, @@ -213,11 +214,11 @@ impl Response::Declined | Response::Error => ( Err(types::ErrorResponse { code: item.response.response_code, - message: item.response.responsetext, - reason: None, + message: item.response.responsetext.to_owned(), + reason: Some(item.response.responsetext), status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(item.response.transactionid), }), enums::AttemptStatus::Failure, ), @@ -346,12 +347,14 @@ impl let (response, status) = match item.response.response { Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.orderid), + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transactionid, + ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), incremental_authorization_allowed: None, }), if let Some(diesel_models::enums::CaptureMethod::Automatic) = @@ -382,11 +385,11 @@ impl ForeignFrom<(NmiCompleteResponse, u16)> for types::ErrorResponse { fn foreign_from((response, http_code): (NmiCompleteResponse, u16)) -> Self { Self { code: response.response_code, - message: response.responsetext, - reason: None, + message: response.responsetext.to_owned(), + reason: Some(response.responsetext), status_code: http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(response.transactionid), } } } @@ -632,13 +635,13 @@ impl Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), incremental_authorization_allowed: None, }), enums::AttemptStatus::CaptureInitiated, @@ -726,13 +729,13 @@ impl Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), incremental_authorization_allowed: None, }), enums::AttemptStatus::Charged, @@ -757,11 +760,11 @@ impl ForeignFrom<(StandardResponse, u16)> for types::ErrorResponse { fn foreign_from((response, http_code): (StandardResponse, u16)) -> Self { Self { code: response.response_code, - message: response.responsetext, - reason: None, + message: response.responsetext.to_owned(), + reason: Some(response.responsetext), status_code: http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(response.transactionid), } } } @@ -782,13 +785,13 @@ impl TryFrom> Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), incremental_authorization_allowed: None, }), if let Some(diesel_models::enums::CaptureMethod::Automatic) = @@ -832,13 +835,13 @@ impl Response::Approved => ( Ok(types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::ConnectorTransactionId( - item.response.transactionid, + item.response.transactionid.to_owned(), ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.orderid), incremental_authorization_allowed: None, }), enums::AttemptStatus::VoidInitiated, @@ -1163,8 +1166,12 @@ pub enum NmiWebhookEventType { impl ForeignFrom for webhooks::IncomingWebhookEvent { fn foreign_from(status: NmiWebhookEventType) -> Self { match status { - NmiWebhookEventType::SaleSuccess => Self::PaymentIntentSuccess, - NmiWebhookEventType::SaleFailure => Self::PaymentIntentFailure, + NmiWebhookEventType::SaleSuccess | NmiWebhookEventType::CaptureSuccess => { + Self::PaymentIntentSuccess + } + NmiWebhookEventType::SaleFailure | NmiWebhookEventType::CaptureFailure => { + Self::PaymentIntentFailure + } NmiWebhookEventType::RefundSuccess => Self::RefundSuccess, NmiWebhookEventType::RefundFailure => Self::RefundFailure, NmiWebhookEventType::VoidSuccess => Self::PaymentIntentCancelled, @@ -1175,8 +1182,6 @@ impl ForeignFrom for webhooks::IncomingWebhookEvent { | NmiWebhookEventType::AuthUnknown | NmiWebhookEventType::VoidFailure | NmiWebhookEventType::VoidUnknown - | NmiWebhookEventType::CaptureSuccess - | NmiWebhookEventType::CaptureFailure | NmiWebhookEventType::CaptureUnknown => Self::EventNotSupported, } } From 00008c16c1c20f1f34381d0fc7e55ef05183e776 Mon Sep 17 00:00:00 2001 From: Sagar naik Date: Fri, 5 Jan 2024 18:58:29 +0530 Subject: [PATCH 277/443] fix(analytics): fixed response code to 501 (#3119) Co-authored-by: Sampras Lopes --- crates/analytics/src/api_event/core.rs | 21 +++++++++++++-------- crates/analytics/src/sdk_events/core.rs | 19 ++++++++++++------- crates/analytics/src/types.rs | 14 ++++++++++++-- crates/router/src/core/webhooks.rs | 2 +- crates/router/src/events/api_logs.rs | 6 +++--- crates/router/src/services/api.rs | 2 +- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/crates/analytics/src/api_event/core.rs b/crates/analytics/src/api_event/core.rs index b368d6374f75..81b82c5dce15 100644 --- a/crates/analytics/src/api_event/core.rs +++ b/crates/analytics/src/api_event/core.rs @@ -8,6 +8,7 @@ use api_models::analytics::{ AnalyticsMetadata, ApiEventFiltersResponse, GetApiEventFiltersRequest, GetApiEventMetricRequest, MetricsResponse, }; +use common_utils::errors::ReportSwitchExt; use error_stack::{IntoReport, ResultExt}; use router_env::{ instrument, logger, @@ -32,16 +33,18 @@ pub async fn api_events_core( merchant_id: String, ) -> AnalyticsResult> { let data = match pool { - AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) - .into_report() - .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( + "API Events not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), AnalyticsProvider::Clickhouse(pool) => get_api_event(&merchant_id, req, pool).await, AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { get_api_event(&merchant_id, req, ckh_pool).await } } - .change_context(AnalyticsError::UnknownError)?; + .switch()?; Ok(data) } @@ -58,9 +61,11 @@ pub async fn get_filters( let mut res = ApiEventFiltersResponse::default(); for dim in req.group_by_names { let values = match pool { - AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) - .into_report() - .attach_printable("SQL Analytics is not implemented for API Events"), + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented( + "API Events not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for API Events"), AnalyticsProvider::Clickhouse(ckh_pool) | AnalyticsProvider::CombinedSqlx(_, ckh_pool) | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { @@ -68,7 +73,7 @@ pub async fn get_filters( .await } } - .change_context(AnalyticsError::UnknownError)? + .switch()? .into_iter() .filter_map(|fil: ApiEventFilter| match dim { ApiEventDimensions::StatusCode => fil.status_code.map(|i| i.to_string()), diff --git a/crates/analytics/src/sdk_events/core.rs b/crates/analytics/src/sdk_events/core.rs index 34f23c745b05..46cc636f4388 100644 --- a/crates/analytics/src/sdk_events/core.rs +++ b/crates/analytics/src/sdk_events/core.rs @@ -7,6 +7,7 @@ use api_models::analytics::{ AnalyticsMetadata, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, MetricsResponse, SdkEventFiltersResponse, }; +use common_utils::errors::ReportSwitchExt; use error_stack::{IntoReport, ResultExt}; use router_env::{instrument, logger, tracing}; @@ -28,16 +29,18 @@ pub async fn sdk_events_core( publishable_key: String, ) -> AnalyticsResult> { match pool { - AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented) - .into_report() - .attach_printable("SQL Analytics is not implemented for Sdk Events"), + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( + "SDK Events not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for Sdk Events"), AnalyticsProvider::Clickhouse(pool) => get_sdk_event(&publishable_key, req, pool).await, AnalyticsProvider::CombinedSqlx(_sqlx_pool, ckh_pool) | AnalyticsProvider::CombinedCkh(_sqlx_pool, ckh_pool) => { get_sdk_event(&publishable_key, req, ckh_pool).await } } - .change_context(AnalyticsError::UnknownError) + .switch() } #[instrument(skip_all)] @@ -159,9 +162,11 @@ pub async fn get_filters( if let Some(publishable_key) = publishable_key { for dim in req.group_by_names { let values = match pool { - AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented) - .into_report() - .attach_printable("SQL Analytics is not implemented for SDK Events"), + AnalyticsProvider::Sqlx(_pool) => Err(FiltersError::NotImplemented( + "SDK Events not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for SDK Events"), AnalyticsProvider::Clickhouse(pool) => { get_sdk_event_filter_for_dimension(dim, publishable_key, &req.time_range, pool) .await diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 16d342d3d2ee..8b1bdbd1ab92 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -8,6 +8,7 @@ use common_utils::{ use error_stack::{report, Report, ResultExt}; use super::query::QueryBuildingError; +use crate::errors::AnalyticsError; #[derive(serde::Deserialize, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] @@ -124,8 +125,8 @@ pub enum FiltersError { #[error("Error running Query")] QueryExecutionFailure, #[allow(dead_code)] - #[error("Not Implemented")] - NotImplemented, + #[error("Not Implemented: {0}")] + NotImplemented(&'static str), } impl ErrorSwitch for QueryBuildingError { @@ -134,4 +135,13 @@ impl ErrorSwitch for QueryBuildingError { } } +impl ErrorSwitch for FiltersError { + fn switch(&self) -> AnalyticsError { + match self { + Self::QueryBuildingError | Self::QueryExecutionFailure => AnalyticsError::UnknownError, + Self::NotImplemented(a) => AnalyticsError::NotImplemented(a), + } + } +} + impl_misc_api_event_type!(AnalyticsDomain); diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index c7e7548f00bd..4354a3ee1959 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -938,7 +938,7 @@ pub async fn webhooks_wrapper { diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index bfc10f722c1f..42017f4500ed 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -41,7 +41,7 @@ pub struct ApiEvent { #[serde(flatten)] event_type: ApiEventsType, hs_latency: Option, - http_method: Option, + http_method: String, } impl ApiEvent { @@ -59,7 +59,7 @@ impl ApiEvent { error: Option, event_type: ApiEventsType, http_req: &HttpRequest, - http_method: Option, + http_method: &http::Method, ) -> Self { Self { merchant_id, @@ -83,7 +83,7 @@ impl ApiEvent { url_path: http_req.path().to_string(), event_type, hs_latency, - http_method, + http_method: http_method.to_string(), } } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 9ac8d5e5eb41..8298d9a105bf 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -938,7 +938,7 @@ where error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, - Some(request.method().to_string()), + request.method(), ); match api_event.clone().try_into() { Ok(event) => { From d152c3a1ca70c39f5c64edf63b5995f6cf02c88a Mon Sep 17 00:00:00 2001 From: Sagar naik Date: Fri, 5 Jan 2024 18:58:55 +0530 Subject: [PATCH 278/443] fix(analytics): added response to the connector outgoing event (#3129) Co-authored-by: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Co-authored-by: Sampras lopes --- .../{api_events_v2.sql => api_events.sql} | 32 ++++-- .../clickhouse/scripts/connector_events.sql | 97 ++++++++++++++++ crates/common_utils/src/events.rs | 5 +- crates/router/src/events/api_logs.rs | 13 ++- crates/router/src/services/api.rs | 12 +- crates/router/src/services/kafka.rs | 8 -- crates/router/src/services/kafka/api_event.rs | 108 ------------------ .../src/services/kafka/outgoing_request.rs | 19 --- 8 files changed, 145 insertions(+), 149 deletions(-) rename crates/analytics/docs/clickhouse/scripts/{api_events_v2.sql => api_events.sql} (83%) create mode 100644 crates/analytics/docs/clickhouse/scripts/connector_events.sql delete mode 100644 crates/router/src/services/kafka/api_event.rs delete mode 100644 crates/router/src/services/kafka/outgoing_request.rs diff --git a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql b/crates/analytics/docs/clickhouse/scripts/api_events.sql similarity index 83% rename from crates/analytics/docs/clickhouse/scripts/api_events_v2.sql rename to crates/analytics/docs/clickhouse/scripts/api_events.sql index 33f158ce48b7..c3fc3d7b06d5 100644 --- a/crates/analytics/docs/clickhouse/scripts/api_events_v2.sql +++ b/crates/analytics/docs/clickhouse/scripts/api_events.sql @@ -1,4 +1,4 @@ -CREATE TABLE api_events_v2_queue ( +CREATE TABLE api_events_queue ( `merchant_id` String, `payment_id` Nullable(String), `refund_id` Nullable(String), @@ -14,12 +14,15 @@ CREATE TABLE api_events_v2_queue ( `api_auth_type` LowCardinality(String), `request` String, `response` Nullable(String), + `error` Nullable(String), `authentication_data` Nullable(String), `status_code` UInt32, - `created_at` DateTime CODEC(T64, LZ4), + `created_at_timestamp` DateTime64(3), `latency` UInt128, `user_agent` String, `ip_addr` String, + `hs_latency` Nullable(UInt128), + `http_method` LowCardinality(String), `url_path` String ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-api-log-events', @@ -28,7 +31,7 @@ kafka_format = 'JSONEachRow', kafka_handle_error_mode = 'stream'; -CREATE TABLE api_events_v2_dist ( +CREATE TABLE api_events_dist ( `merchant_id` String, `payment_id` Nullable(String), `refund_id` Nullable(String), @@ -44,13 +47,15 @@ CREATE TABLE api_events_v2_dist ( `api_auth_type` LowCardinality(String), `request` String, `response` Nullable(String), + `error` Nullable(String), `authentication_data` Nullable(String), `status_code` UInt32, - `created_at` DateTime CODEC(T64, LZ4), - `inserted_at` DateTime CODEC(T64, LZ4), + `created_at_timestamp` DateTime64(3), `latency` UInt128, `user_agent` String, `ip_addr` String, + `hs_latency` Nullable(UInt128), + `http_method` LowCardinality(String), `url_path` String, INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, @@ -62,7 +67,7 @@ ORDER BY TTL created_at + toIntervalMonth(6) ; -CREATE MATERIALIZED VIEW api_events_v2_mv TO api_events_v2_dist ( +CREATE MATERIALIZED VIEW api_events_mv TO api_events_dist ( `merchant_id` String, `payment_id` Nullable(String), `refund_id` Nullable(String), @@ -78,13 +83,15 @@ CREATE MATERIALIZED VIEW api_events_v2_mv TO api_events_v2_dist ( `api_auth_type` LowCardinality(String), `request` String, `response` Nullable(String), + `error` Nullable(String), `authentication_data` Nullable(String), `status_code` UInt32, - `created_at` DateTime CODEC(T64, LZ4), - `inserted_at` DateTime CODEC(T64, LZ4), + `created_at_timestamp` DateTime64(3), `latency` UInt128, `user_agent` String, `ip_addr` String, + `hs_latency` Nullable(UInt128), + `http_method` LowCardinality(String), `url_path` String ) AS SELECT @@ -103,16 +110,19 @@ SELECT api_auth_type, request, response, + error, authentication_data, status_code, - created_at, + created_at_timestamp, now() as inserted_at, latency, user_agent, ip_addr, + hs_latency, + http_method, url_path FROM - api_events_v2_queue + api_events_queue where length(_error) = 0; @@ -133,6 +143,6 @@ SELECT _offset AS offset, _raw_message AS raw, _error AS error -FROM api_events_v2_queue +FROM api_events_queue WHERE length(_error) > 0 ; diff --git a/crates/analytics/docs/clickhouse/scripts/connector_events.sql b/crates/analytics/docs/clickhouse/scripts/connector_events.sql new file mode 100644 index 000000000000..5821cd035567 --- /dev/null +++ b/crates/analytics/docs/clickhouse/scripts/connector_events.sql @@ -0,0 +1,97 @@ +CREATE TABLE connector_events_queue ( + `merchant_id` String, + `payment_id` Nullable(String), + `connector_name` LowCardinality(String), + `request_id` String, + `flow` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `status_code` UInt32, + `created_at` DateTime64(3), + `latency` UInt128, + `method` LowCardinality(String) +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', +kafka_topic_list = 'hyperswitch-connector-api-events', +kafka_group_name = 'hyper-c1', +kafka_format = 'JSONEachRow', +kafka_handle_error_mode = 'stream'; + + +CREATE TABLE connector_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `connector_name` LowCardinality(String), + `request_id` String, + `flow` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `status_code` UInt32, + `created_at` DateTime64(3), + `inserted_at` DateTime64(3), + `latency` UInt128, + `method` LowCardinality(String), + INDEX flowIndex flowTYPE bloom_filter GRANULARITY 1, + INDEX connectorIndex connector_name TYPE bloom_filter GRANULARITY 1, + INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 +) ENGINE = MergeTree +PARTITION BY toStartOfDay(created_at) +ORDER BY + (created_at, merchant_id, flow_type, status_code, api_flow) +TTL created_at + toIntervalMonth(6) +; + +CREATE MATERIALIZED VIEW connector_events_mv TO connector_events_dist ( + `merchant_id` String, + `payment_id` Nullable(String), + `connector_name` LowCardinality(String), + `request_id` String, + `flow` LowCardinality(String), + `request` String, + `response` Nullable(String), + `error` Nullable(String), + `status_code` UInt32, + `created_at` DateTime64(3), + `latency` UInt128, + `method` LowCardinality(String) +) AS +SELECT + merchant_id, + payment_id, + connector_name, + request_id, + flow, + request, + response, + error, + status_code, + created_at, + now() as inserted_at, + latency, + method, +FROM + connector_events_queue +where length(_error) = 0; + + +CREATE MATERIALIZED VIEW connector_events_parse_errors +( + `topic` String, + `partition` Int64, + `offset` Int64, + `raw` String, + `error` String +) +ENGINE = MergeTree +ORDER BY (topic, partition, offset) +SETTINGS index_granularity = 8192 AS +SELECT + _topic AS topic, + _partition AS partition, + _offset AS offset, + _raw_message AS raw, + _error AS error +FROM connector_events_queue +WHERE length(_error) > 0 +; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index c9efbb73c208..6bbf78afe421 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -40,7 +40,10 @@ pub enum ApiEventsType { }, Routing, ResourceListAPI, - PaymentRedirectionResponse, + PaymentRedirectionResponse { + connector: Option, + payment_id: Option, + }, Gsm, // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 42017f4500ed..78a66d2f04e7 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -116,7 +116,6 @@ impl_misc_api_event_type!( AttachEvidenceRequest, DisputeId, PaymentLinkFormData, - PaymentsRedirectResponseData, ConfigUpdate ); @@ -131,3 +130,15 @@ impl_misc_api_event_type!( DummyConnectorRefundResponse, DummyConnectorRefundRequest ); + +impl ApiEventMetric for PaymentsRedirectResponseData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentRedirectionResponse { + connector: self.connector.clone(), + payment_id: match &self.resource_id { + api_models::payments::PaymentIdType::PaymentIntentId(id) => Some(id.clone()), + _ => None, + }, + }) + } +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 8298d9a105bf..ba3607fb7dc5 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -377,7 +377,17 @@ where req.connector.clone(), std::any::type_name::(), masked_request_body, - None, + response + .as_ref() + .map(|response| { + response + .as_ref() + .map_or_else(|value| value, |value| value) + .response + .escape_ascii() + .to_string() + }) + .ok(), request_url, request_method, req.payment_id.clone(), diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs index 5a6d7043e6d0..2b29a61b4a4f 100644 --- a/crates/router/src/services/kafka.rs +++ b/crates/router/src/services/kafka.rs @@ -8,12 +8,9 @@ use rdkafka::{ }; use crate::events::EventType; -mod api_event; -pub mod outgoing_request; mod payment_attempt; mod payment_intent; mod refund; -pub use api_event::{ApiCallEventType, ApiEvents, ApiEventsType}; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::refund::Refund; use serde::Serialize; @@ -300,11 +297,6 @@ impl KafkaProducer { }) } - pub async fn log_api_event(&self, event: &ApiEvents) -> MQResult<()> { - self.log_kafka_event(&self.api_logs_topic, event) - .attach_printable_lazy(|| format!("Failed to add api log event {event:?}")) - } - pub fn get_topic(&self, event: EventType) -> &str { match event { EventType::ApiLogs => &self.api_logs_topic, diff --git a/crates/router/src/services/kafka/api_event.rs b/crates/router/src/services/kafka/api_event.rs deleted file mode 100644 index 7de271915927..000000000000 --- a/crates/router/src/services/kafka/api_event.rs +++ /dev/null @@ -1,108 +0,0 @@ -use api_models::enums as api_enums; -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(tag = "flow_type")] -pub enum ApiEventsType { - Payment { - payment_id: String, - }, - Refund { - payment_id: String, - refund_id: String, - }, - Default, - PaymentMethod { - payment_method_id: String, - payment_method: Option, - payment_method_type: Option, - }, - Customer { - customer_id: String, - }, - User { - //specified merchant_id will overridden on global defined - merchant_id: String, - user_id: String, - }, - Webhooks { - connector: String, - payment_id: Option, - }, - OutgoingEvent, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct ApiEvents { - pub api_name: String, - pub request_id: Option, - //It is require to solve ambiquity in case of event_type is User - #[serde(skip_serializing_if = "Option::is_none")] - pub merchant_id: Option, - pub request: String, - pub response: String, - pub status_code: u16, - #[serde(with = "time::serde::timestamp")] - pub created_at: OffsetDateTime, - pub latency: u128, - //conflicting fields underlying enums will be used - #[serde(flatten)] - pub event_type: ApiEventsType, - pub user_agent: Option, - pub ip_addr: Option, - pub url_path: Option, - pub api_event_type: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum ApiCallEventType { - IncomingApiEvent, - OutgoingApiEvent, -} - -impl super::KafkaMessage for ApiEvents { - fn key(&self) -> String { - match &self.event_type { - ApiEventsType::Payment { payment_id } => format!( - "{}_{}", - self.merchant_id - .as_ref() - .unwrap_or(&"default_merchant_id".to_string()), - payment_id - ), - ApiEventsType::Refund { - payment_id, - refund_id, - } => format!("{payment_id}_{refund_id}"), - ApiEventsType::Default => "key".to_string(), - ApiEventsType::PaymentMethod { - payment_method_id, - payment_method, - payment_method_type, - } => format!( - "{:?}_{:?}_{:?}", - payment_method_id.clone(), - payment_method.clone(), - payment_method_type.clone(), - ), - ApiEventsType::Customer { customer_id } => customer_id.to_string(), - ApiEventsType::User { - merchant_id, - user_id, - } => format!("{}_{}", merchant_id, user_id), - ApiEventsType::Webhooks { - connector, - payment_id, - } => format!( - "webhook_{}_{connector}", - payment_id.clone().unwrap_or_default() - ), - ApiEventsType::OutgoingEvent => "outgoing_event".to_string(), - } - } - - fn creation_timestamp(&self) -> Option { - Some(self.created_at.unix_timestamp()) - } -} diff --git a/crates/router/src/services/kafka/outgoing_request.rs b/crates/router/src/services/kafka/outgoing_request.rs deleted file mode 100644 index bb09fe91fe6d..000000000000 --- a/crates/router/src/services/kafka/outgoing_request.rs +++ /dev/null @@ -1,19 +0,0 @@ -use reqwest::Url; - -pub struct OutgoingRequest { - pub url: Url, - pub latency: u128, -} - -// impl super::KafkaMessage for OutgoingRequest { -// fn key(&self) -> String { -// format!( -// "{}_{}", - -// ) -// } - -// fn creation_timestamp(&self) -> Option { -// Some(self.created_at.unix_timestamp()) -// } -// } From 7ea50c3a78bc1a091077c23999a69dda1cf0f463 Mon Sep 17 00:00:00 2001 From: Jeeva Ramachandran <120017870+JeevaRamu0104@users.noreply.github.com> Date: Mon, 8 Jan 2024 11:25:05 +0530 Subject: [PATCH 279/443] refactor(euclid_wasm): Update wasm config (#3222) --- crates/connector_configs/src/common_config.rs | 47 +- crates/connector_configs/src/connector.rs | 55 +- .../src/response_modifier.rs | 121 +- crates/connector_configs/src/transformer.rs | 14 +- .../connector_configs/toml/development.toml | 2387 +++++++++++++--- crates/connector_configs/toml/production.toml | 1622 +++++++++-- crates/connector_configs/toml/sandbox.toml | 2398 ++++++++++++++--- 7 files changed, 5682 insertions(+), 962 deletions(-) diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs index 6ba44d4ed7eb..c3cbaab4ab33 100644 --- a/crates/connector_configs/src/common_config.rs +++ b/crates/connector_configs/src/common_config.rs @@ -83,6 +83,49 @@ pub struct ApiModelMetaData { pub apple_pay_combined: Option, } +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct CardProvider { + pub payment_method_type: api_models::enums::CardNetwork, + /// List of currencies accepted or has the processing capabilities of the processor + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["USD", "INR"] + } + ), value_type = Option)] + pub accepted_currencies: Option, + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["UK", "AU"] + } + ), value_type = Option)] + pub accepted_countries: Option, +} +#[serde_with::skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Provider { + pub payment_method_type: api_models::enums::PaymentMethodType, + /// List of currencies accepted or has the processing capabilities of the processor + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["USD", "INR"] + } + ), value_type = Option)] + pub accepted_currencies: Option, + #[schema(example = json!( + { + "type": "specific_accepted", + "list": ["UK", "AU"] + } + ), value_type = Option)] + pub accepted_countries: Option, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct ConnectorApiIntegrationPayload { @@ -105,8 +148,8 @@ pub struct ConnectorApiIntegrationPayload { pub struct DashboardPaymentMethodPayload { pub payment_method: api_models::enums::PaymentMethod, pub payment_method_type: String, - pub provider: Option>, - pub card_provider: Option>, + pub provider: Option>, + pub card_provider: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index f41fa4aab457..f0997d53107e 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -2,24 +2,31 @@ use std::collections::HashMap; #[cfg(feature = "payouts")] use api_models::enums::PayoutConnectors; -use api_models::{ - enums::{CardNetwork, Connector, PaymentMethodType}, - payments, -}; +use api_models::{enums::Connector, payments}; use serde::Deserialize; #[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] use toml; -use crate::common_config::{GooglePayData, ZenApplePay}; +use crate::common_config::{CardProvider, GooglePayData, Provider, ZenApplePay}; + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Classic { + pub password_classic: String, + pub username_classic: String, + pub merchant_id_classic: String, +} + +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Evoucher { + pub password_evoucher: String, + pub username_evoucher: String, + pub merchant_id_evoucher: String, +} #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CurrencyAuthKeyType { - pub password_classic: Option, - pub username_classic: Option, - pub merchant_id_classic: Option, - pub password_evoucher: Option, - pub username_evoucher: Option, - pub merchant_id_evoucher: Option, + pub classic: Classic, + pub evoucher: Evoucher, } #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -75,19 +82,19 @@ pub struct ConnectorTomlConfig { pub connector_auth: Option, pub connector_webhook_details: Option, pub metadata: Option, - pub credit: Option>, - pub debit: Option>, - pub bank_transfer: Option>, - pub bank_redirect: Option>, - pub bank_debit: Option>, - pub pay_later: Option>, - pub wallet: Option>, - pub crypto: Option>, - pub reward: Option>, - pub upi: Option>, - pub voucher: Option>, - pub gift_card: Option>, - pub card_redirect: Option>, + pub credit: Option>, + pub debit: Option>, + pub bank_transfer: Option>, + pub bank_redirect: Option>, + pub bank_debit: Option>, + pub pay_later: Option>, + pub wallet: Option>, + pub crypto: Option>, + pub reward: Option>, + pub upi: Option>, + pub voucher: Option>, + pub gift_card: Option>, + pub card_redirect: Option>, pub is_verifiable: Option, } #[serde_with::skip_serializing_none] diff --git a/crates/connector_configs/src/response_modifier.rs b/crates/connector_configs/src/response_modifier.rs index 6a09c58a75ca..80332612c13a 100644 --- a/crates/connector_configs/src/response_modifier.rs +++ b/crates/connector_configs/src/response_modifier.rs @@ -1,23 +1,23 @@ use crate::common_config::{ - ConnectorApiIntegrationPayload, DashboardMetaData, DashboardPaymentMethodPayload, - DashboardRequestPayload, GoogleApiModelData, GooglePayData, GpayDashboardPayLoad, + CardProvider, ConnectorApiIntegrationPayload, DashboardMetaData, DashboardPaymentMethodPayload, + DashboardRequestPayload, GoogleApiModelData, GooglePayData, GpayDashboardPayLoad, Provider, }; impl ConnectorApiIntegrationPayload { pub fn get_transformed_response_payload(response: Self) -> DashboardRequestPayload { - let mut wallet_details = Vec::new(); - let mut bank_redirect_details = Vec::new(); - let mut pay_later_details = Vec::new(); - let mut debit_details = Vec::new(); - let mut credit_details = Vec::new(); - let mut bank_transfer_details = Vec::new(); - let mut crypto_details = Vec::new(); - let mut bank_debit_details = Vec::new(); - let mut reward_details = Vec::new(); - let mut upi_details = Vec::new(); - let mut voucher_details = Vec::new(); - let mut gift_card_details = Vec::new(); - let mut card_redirect_details = Vec::new(); + let mut wallet_details: Vec = Vec::new(); + let mut bank_redirect_details: Vec = Vec::new(); + let mut pay_later_details: Vec = Vec::new(); + let mut debit_details: Vec = Vec::new(); + let mut credit_details: Vec = Vec::new(); + let mut bank_transfer_details: Vec = Vec::new(); + let mut crypto_details: Vec = Vec::new(); + let mut bank_debit_details: Vec = Vec::new(); + let mut reward_details: Vec = Vec::new(); + let mut upi_details: Vec = Vec::new(); + let mut voucher_details: Vec = Vec::new(); + let mut gift_card_details: Vec = Vec::new(); + let mut card_redirect_details: Vec = Vec::new(); if let Some(payment_methods_enabled) = response.payment_methods_enabled.clone() { for methods in payment_methods_enabled { @@ -25,19 +25,35 @@ impl ConnectorApiIntegrationPayload { api_models::enums::PaymentMethod::Card => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - let payment_type = method_type.payment_method_type; - match payment_type { + match method_type.payment_method_type { api_models::enums::PaymentMethodType::Credit => { if let Some(card_networks) = method_type.card_networks { for card in card_networks { - credit_details.push(card) + credit_details.push(CardProvider { + payment_method_type: card, + accepted_currencies: method_type + .accepted_currencies + .clone(), + accepted_countries: method_type + .accepted_countries + .clone(), + }) } } } api_models::enums::PaymentMethodType::Debit => { if let Some(card_networks) = method_type.card_networks { for card in card_networks { - debit_details.push(card) + // debit_details.push(card) + debit_details.push(CardProvider { + payment_method_type: card, + accepted_currencies: method_type + .accepted_currencies + .clone(), + accepted_countries: method_type + .accepted_countries + .clone(), + }) } } } @@ -49,77 +65,122 @@ impl ConnectorApiIntegrationPayload { api_models::enums::PaymentMethod::Wallet => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - wallet_details.push(method_type.payment_method_type) + // wallet_details.push(method_type.payment_method_type) + wallet_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::BankRedirect => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - bank_redirect_details.push(method_type.payment_method_type) + bank_redirect_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::PayLater => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - pay_later_details.push(method_type.payment_method_type) + pay_later_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::BankTransfer => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - bank_transfer_details.push(method_type.payment_method_type) + bank_transfer_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::Crypto => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - crypto_details.push(method_type.payment_method_type) + crypto_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::BankDebit => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - bank_debit_details.push(method_type.payment_method_type) + bank_debit_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::Reward => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - reward_details.push(method_type.payment_method_type) + reward_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::Upi => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - upi_details.push(method_type.payment_method_type) + upi_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::Voucher => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - voucher_details.push(method_type.payment_method_type) + voucher_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::GiftCard => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - gift_card_details.push(method_type.payment_method_type) + gift_card_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } api_models::enums::PaymentMethod::CardRedirect => { if let Some(payment_method_types) = methods.payment_method_types { for method_type in payment_method_types { - card_redirect_details.push(method_type.payment_method_type) + card_redirect_details.push(Provider { + payment_method_type: method_type.payment_method_type, + accepted_currencies: method_type.accepted_currencies.clone(), + accepted_countries: method_type.accepted_countries.clone(), + }) } } } diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs index aff75128e9cf..be68a0c5f94b 100644 --- a/crates/connector_configs/src/transformer.rs +++ b/crates/connector_configs/src/transformer.rs @@ -10,7 +10,7 @@ use api_models::{ use crate::common_config::{ ApiModelMetaData, ConnectorApiIntegrationPayload, DashboardMetaData, DashboardRequestPayload, - GoogleApiModelData, GooglePayData, PaymentMethodsEnabled, + GoogleApiModelData, GooglePayData, PaymentMethodsEnabled, Provider, }; impl DashboardRequestPayload { @@ -63,23 +63,23 @@ impl DashboardRequestPayload { } pub fn transform_payment_method( connector: Connector, - provider: Vec, + provider: Vec, payment_method: PaymentMethod, ) -> Vec { let mut payment_method_types = Vec::new(); for method_type in provider { let data = payment_methods::RequestPaymentMethodTypes { - payment_method_type: method_type, + payment_method_type: method_type.payment_method_type, card_networks: None, minimum_amount: Some(0), maximum_amount: Some(68607706), recurring_enabled: true, installment_payment_enabled: false, - accepted_currencies: None, - accepted_countries: None, + accepted_currencies: method_type.accepted_currencies, + accepted_countries: method_type.accepted_countries, payment_experience: Self::get_payment_experience( connector, - method_type, + method_type.payment_method_type, payment_method, ), }; @@ -109,7 +109,7 @@ impl DashboardRequestPayload { for method in card_provider { let data = payment_methods::RequestPaymentMethodTypes { payment_method_type: payment_type, - card_networks: Some(vec![method]), + card_networks: Some(vec![method.payment_method_type]), minimum_amount: Some(0), maximum_amount: Some(68607706), recurring_enabled: true, diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index b69b1aabaff2..b24de92de101 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1,32 +1,235 @@ - [aci] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["ali_pay","mb_way"] -bank_redirect=["ideal","giropay","sofort","eps","przelewy24","trustly","interac"] +[[aci.credit]] + payment_method_type = "Mastercard" +[[aci.credit]] + payment_method_type = "Visa" +[[aci.credit]] + payment_method_type = "Interac" +[[aci.credit]] + payment_method_type = "AmericanExpress" +[[aci.credit]] + payment_method_type = "JCB" +[[aci.credit]] + payment_method_type = "DinersClub" +[[aci.credit]] + payment_method_type = "Discover" +[[aci.credit]] + payment_method_type = "CartesBancaires" +[[aci.credit]] + payment_method_type = "UnionPay" +[[aci.debit]] + payment_method_type = "Mastercard" +[[aci.debit]] + payment_method_type = "Visa" +[[aci.debit]] + payment_method_type = "Interac" +[[aci.debit]] + payment_method_type = "AmericanExpress" +[[aci.debit]] + payment_method_type = "JCB" +[[aci.debit]] + payment_method_type = "DinersClub" +[[aci.debit]] + payment_method_type = "Discover" +[[aci.debit]] + payment_method_type = "CartesBancaires" +[[aci.debit]] + payment_method_type = "UnionPay" +[[aci.wallet]] + payment_method_type = "ali_pay" +[[aci.wallet]] + payment_method_type = "mb_way" +[[aci.bank_redirect]] + payment_method_type = "ideal" +[[aci.bank_redirect]] + payment_method_type = "giropay" +[[aci.bank_redirect]] + payment_method_type = "sofort" +[[aci.bank_redirect]] + payment_method_type = "eps" +[[aci.bank_redirect]] + payment_method_type = "przelewy24" +[[aci.bank_redirect]] + payment_method_type = "trustly" +[[aci.bank_redirect]] + payment_method_type = "interac" [aci.connector_auth.BodyKey] api_key="API Key" key1="Entity ID" [aci.connector_webhook_details] merchant_secret="Source verification key" + [adyen] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","affirm","afterpay_clearpay","pay_bright","walley", "alma", "atome"] -bank_debit=["ach","bacs","sepa"] -bank_redirect=["ideal","giropay","sofort","eps","blik","przelewy24","trustly","online_banking_czech_republic","online_banking_finland","online_banking_poland","online_banking_slovakia","bancontact_card", "online_banking_fpx", "online_banking_thailand", "bizum", "open_banking_uk"] -bank_transfer = ["permata_bank_transfer", "bca_bank_transfer", "bni_va", "bri_va", "cimb_va", "danamon_va", "mandiri_va"] -wallet = ["apple_pay","google_pay","paypal","we_chat_pay","ali_pay","mb_way", "ali_pay_hk", "go_pay", "kakao_pay", "twint", "gcash", "vipps", "dana", "momo", "swish", "touch_n_go"] -voucher = ["boleto", "alfamart", "indomaret", "oxxo", "seven_eleven", "lawson", "mini_stop", "family_mart", "seicomart", "pay_easy"] -gift_card = ["pay_safe_card", "givex"] -card_redirect = ["benefit", "knet", "momo_atm"] +[[adyen.credit]] + payment_method_type = "Mastercard" +[[adyen.credit]] + payment_method_type = "Visa" +[[adyen.credit]] + payment_method_type = "Interac" +[[adyen.credit]] + payment_method_type = "AmericanExpress" +[[adyen.credit]] + payment_method_type = "JCB" +[[adyen.credit]] + payment_method_type = "DinersClub" +[[adyen.credit]] + payment_method_type = "Discover" +[[adyen.credit]] + payment_method_type = "CartesBancaires" +[[adyen.credit]] + payment_method_type = "UnionPay" +[[adyen.debit]] + payment_method_type = "Mastercard" +[[adyen.debit]] + payment_method_type = "Visa" +[[adyen.debit]] + payment_method_type = "Interac" +[[adyen.debit]] + payment_method_type = "AmericanExpress" +[[adyen.debit]] + payment_method_type = "JCB" +[[adyen.debit]] + payment_method_type = "DinersClub" +[[adyen.debit]] + payment_method_type = "Discover" +[[adyen.debit]] + payment_method_type = "CartesBancaires" +[[adyen.debit]] + payment_method_type = "UnionPay" +[[adyen.pay_later]] + payment_method_type = "klarna" +[[adyen.pay_later]] + payment_method_type = "affirm" +[[adyen.pay_later]] + payment_method_type = "afterpay_clearpay" +[[adyen.pay_later]] + payment_method_type = "pay_bright" +[[adyen.pay_later]] + payment_method_type = "walley" +[[adyen.pay_later]] + payment_method_type = "alma" +[[adyen.pay_later]] + payment_method_type = "atome" +[[adyen.bank_debit]] + payment_method_type = "ach" +[[adyen.bank_debit]] + payment_method_type = "bacs" +[[adyen.bank_debit]] + payment_method_type = "sepa" +[[adyen.bank_redirect]] + payment_method_type = "ideal" +[[adyen.bank_redirect]] + payment_method_type = "giropay" +[[adyen.bank_redirect]] + payment_method_type = "sofort" +[[adyen.bank_redirect]] + payment_method_type = "eps" +[[adyen.bank_redirect]] + payment_method_type = "blik" +[[adyen.bank_redirect]] + payment_method_type = "przelewy24" +[[adyen.bank_redirect]] + payment_method_type = "trustly" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_czech_republic" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_finland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_poland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_slovakia" +[[adyen.bank_redirect]] + payment_method_type = "bancontact_card" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_fpx" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_thailand" +[[adyen.bank_redirect]] + payment_method_type = "bizum" +[[adyen.bank_redirect]] + payment_method_type = "open_banking_uk" +[[adyen.bank_transfer]] + payment_method_type = "permata_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bca_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bni_va" +[[adyen.bank_transfer]] + payment_method_type = "bri_va" +[[adyen.bank_transfer]] + payment_method_type = "cimb_va" +[[adyen.bank_transfer]] + payment_method_type = "danamon_va" +[[adyen.bank_transfer]] + payment_method_type = "mandiri_va" +[[adyen.wallet]] + payment_method_type = "apple_pay" +[[adyen.wallet]] + payment_method_type = "google_pay" +[[adyen.wallet]] + payment_method_type = "paypal" +[[adyen.wallet]] + payment_method_type = "we_chat_pay" +[[adyen.wallet]] + payment_method_type = "ali_pay" +[[adyen.wallet]] + payment_method_type = "mb_way" +[[adyen.wallet]] + payment_method_type = "ali_pay_hk" +[[adyen.wallet]] + payment_method_type = "go_pay" +[[adyen.wallet]] + payment_method_type = "kakao_pay" +[[adyen.wallet]] + payment_method_type = "twint" +[[adyen.wallet]] + payment_method_type = "gcash" +[[adyen.wallet]] + payment_method_type = "vipps" +[[adyen.wallet]] + payment_method_type = "dana" +[[adyen.wallet]] + payment_method_type = "momo" +[[adyen.wallet]] + payment_method_type = "swish" +[[adyen.wallet]] + payment_method_type = "touch_n_go" +[[adyen.voucher]] + payment_method_type = "boleto" +[[adyen.voucher]] + payment_method_type = "alfamart" +[[adyen.voucher]] + payment_method_type = "indomaret" +[[adyen.voucher]] + payment_method_type = "oxxo" +[[adyen.voucher]] + payment_method_type = "seven_eleven" +[[adyen.voucher]] + payment_method_type = "lawson" +[[adyen.voucher]] + payment_method_type = "mini_stop" +[[adyen.voucher]] + payment_method_type = "family_mart" +[[adyen.voucher]] + payment_method_type = "seicomart" +[[adyen.voucher]] + payment_method_type = "pay_easy" +[[adyen.gift_card]] + payment_method_type = "pay_safe_card" +[[adyen.gift_card]] + payment_method_type = "givex" +[[adyen.card_redirect]] + payment_method_type = "benefit" +[[adyen.card_redirect]] + payment_method_type = "knet" +[[adyen.card_redirect]] + payment_method_type = "momo_atm" [adyen.connector_auth.BodyKey] api_key="Adyen API Key" key1="Adyen Account Id" [adyen.connector_webhook_details] merchant_secret="Source verification key" - [adyen.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" @@ -44,24 +247,93 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" - - [airwallex] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay"] -body_type="BodyKey" +[[airwallex.credit]] + payment_method_type = "Mastercard" +[[airwallex.credit]] + payment_method_type = "Visa" +[[airwallex.credit]] + payment_method_type = "Interac" +[[airwallex.credit]] + payment_method_type = "AmericanExpress" +[[airwallex.credit]] + payment_method_type = "JCB" +[[airwallex.credit]] + payment_method_type = "DinersClub" +[[airwallex.credit]] + payment_method_type = "Discover" +[[airwallex.credit]] + payment_method_type = "CartesBancaires" +[[airwallex.credit]] + payment_method_type = "UnionPay" +[[airwallex.debit]] + payment_method_type = "Mastercard" +[[airwallex.debit]] + payment_method_type = "Visa" +[[airwallex.debit]] + payment_method_type = "Interac" +[[airwallex.debit]] + payment_method_type = "AmericanExpress" +[[airwallex.debit]] + payment_method_type = "JCB" +[[airwallex.debit]] + payment_method_type = "DinersClub" +[[airwallex.debit]] + payment_method_type = "Discover" +[[airwallex.debit]] + payment_method_type = "CartesBancaires" +[[airwallex.debit]] + payment_method_type = "UnionPay" +[[airwallex.wallet]] + payment_method_type = "google_pay" [airwallex.connector_auth.BodyKey] api_key="API Key" key1="Client ID" [airwallex.connector_webhook_details] merchant_secret="Source verification key" + [authorizedotnet] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","paypal"] -body_type="BodyKey" +[[authorizedotnet.credit]] + payment_method_type = "Mastercard" +[[authorizedotnet.credit]] + payment_method_type = "Visa" +[[authorizedotnet.credit]] + payment_method_type = "Interac" +[[authorizedotnet.credit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.credit]] + payment_method_type = "JCB" +[[authorizedotnet.credit]] + payment_method_type = "DinersClub" +[[authorizedotnet.credit]] + payment_method_type = "Discover" +[[authorizedotnet.credit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.credit]] + payment_method_type = "UnionPay" +[[authorizedotnet.debit]] + payment_method_type = "Mastercard" +[[authorizedotnet.debit]] + payment_method_type = "Visa" +[[authorizedotnet.debit]] + payment_method_type = "Interac" +[[authorizedotnet.debit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.debit]] + payment_method_type = "JCB" +[[authorizedotnet.debit]] + payment_method_type = "DinersClub" +[[authorizedotnet.debit]] + payment_method_type = "Discover" +[[authorizedotnet.debit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.debit]] + payment_method_type = "UnionPay" +[[authorizedotnet.wallet]] + payment_method_type = "google_pay" +[[authorizedotnet.wallet]] + payment_method_type = "paypal" [authorizedotnet.connector_auth.BodyKey] api_key="API Login ID" key1="Transaction Key" @@ -72,17 +344,178 @@ merchant_id="Google Pay Merchant ID" [authorizedotnet.connector_webhook_details] merchant_secret="Source verification key" +[bambora] +[[bambora.credit]] + payment_method_type = "Mastercard" +[[bambora.credit]] + payment_method_type = "Visa" +[[bambora.credit]] + payment_method_type = "Interac" +[[bambora.credit]] + payment_method_type = "AmericanExpress" +[[bambora.credit]] + payment_method_type = "JCB" +[[bambora.credit]] + payment_method_type = "DinersClub" +[[bambora.credit]] + payment_method_type = "Discover" +[[bambora.credit]] + payment_method_type = "CartesBancaires" +[[bambora.credit]] + payment_method_type = "UnionPay" +[[bambora.debit]] + payment_method_type = "Mastercard" +[[bambora.debit]] + payment_method_type = "Visa" +[[bambora.debit]] + payment_method_type = "Interac" +[[bambora.debit]] + payment_method_type = "AmericanExpress" +[[bambora.debit]] + payment_method_type = "JCB" +[[bambora.debit]] + payment_method_type = "DinersClub" +[[bambora.debit]] + payment_method_type = "Discover" +[[bambora.debit]] + payment_method_type = "CartesBancaires" +[[bambora.debit]] + payment_method_type = "UnionPay" +[[bambora.wallet]] + payment_method_type = "apple_pay" +[[bambora.wallet]] + payment_method_type = "paypal" +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica] +[[bankofamerica.credit]] + payment_method_type = "Mastercard" +[[bankofamerica.credit]] + payment_method_type = "Visa" +[[bankofamerica.credit]] + payment_method_type = "Interac" +[[bankofamerica.credit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.credit]] + payment_method_type = "JCB" +[[bankofamerica.credit]] + payment_method_type = "DinersClub" +[[bankofamerica.credit]] + payment_method_type = "Discover" +[[bankofamerica.credit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.credit]] + payment_method_type = "UnionPay" +[[bankofamerica.debit]] + payment_method_type = "Mastercard" +[[bankofamerica.debit]] + payment_method_type = "Visa" +[[bankofamerica.debit]] + payment_method_type = "Interac" +[[bankofamerica.debit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.debit]] + payment_method_type = "JCB" +[[bankofamerica.debit]] + payment_method_type = "DinersClub" +[[bankofamerica.debit]] + payment_method_type = "Discover" +[[bankofamerica.debit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.debit]] + payment_method_type = "UnionPay" +[[bankofamerica.wallet]] + payment_method_type = "apple_pay" +[[bankofamerica.wallet]] + payment_method_type = "google_pay" +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + [bitpay] -crypto = ["crypto_currency"] +[[bitpay.crypto]] + payment_method_type = "crypto_currency" [bitpay.connector_auth.HeaderKey] api_key="API Key" [bitpay.connector_webhook_details] merchant_secret="Source verification key" [bluesnap] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","apple_pay"] +[[bluesnap.credit]] + payment_method_type = "Mastercard" +[[bluesnap.credit]] + payment_method_type = "Visa" +[[bluesnap.credit]] + payment_method_type = "Interac" +[[bluesnap.credit]] + payment_method_type = "AmericanExpress" +[[bluesnap.credit]] + payment_method_type = "JCB" +[[bluesnap.credit]] + payment_method_type = "DinersClub" +[[bluesnap.credit]] + payment_method_type = "Discover" +[[bluesnap.credit]] + payment_method_type = "CartesBancaires" +[[bluesnap.credit]] + payment_method_type = "UnionPay" +[[bluesnap.debit]] + payment_method_type = "Mastercard" +[[bluesnap.debit]] + payment_method_type = "Visa" +[[bluesnap.debit]] + payment_method_type = "Interac" +[[bluesnap.debit]] + payment_method_type = "AmericanExpress" +[[bluesnap.debit]] + payment_method_type = "JCB" +[[bluesnap.debit]] + payment_method_type = "DinersClub" +[[bluesnap.debit]] + payment_method_type = "Discover" +[[bluesnap.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[bluesnap.wallet]] + payment_method_type = "google_pay" +[[bluesnap.wallet]] + payment_method_type = "apple_pay" [bluesnap.connector_auth.BodyKey] api_key="Password" key1="Username" @@ -108,10 +541,63 @@ label="apple" [bluesnap.metadata] merchant_id="Merchant Id" +[boku] +[[boku.wallet]] + payment_method_type = "dana" +[[boku.wallet]] + payment_method_type = "gcash" +[[boku.wallet]] + payment_method_type = "go_pay" +[[boku.wallet]] + payment_method_type = "kakao_pay" +[[boku.wallet]] + payment_method_type = "momo" +[boku.connector_auth.BodyKey] +api_key="API KEY" +key1= "MERCHANT ID" +[boku.connector_webhook_details] +merchant_secret="Source verification key" + [braintree] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[braintree.credit]] + payment_method_type = "Mastercard" +[[braintree.credit]] + payment_method_type = "Visa" +[[braintree.credit]] + payment_method_type = "Interac" +[[braintree.credit]] + payment_method_type = "AmericanExpress" +[[braintree.credit]] + payment_method_type = "JCB" +[[braintree.credit]] + payment_method_type = "DinersClub" +[[braintree.credit]] + payment_method_type = "Discover" +[[braintree.credit]] + payment_method_type = "CartesBancaires" +[[braintree.credit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "Mastercard" +[[braintree.debit]] + payment_method_type = "Visa" +[[braintree.debit]] + payment_method_type = "Interac" +[[braintree.debit]] + payment_method_type = "AmericanExpress" +[[braintree.debit]] + payment_method_type = "JCB" +[[braintree.debit]] + payment_method_type = "DinersClub" +[[braintree.debit]] + payment_method_type = "Discover" +[[braintree.debit]] + payment_method_type = "CartesBancaires" +[[braintree.debit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "UnionPay" [braintree.connector_webhook_details] merchant_secret="Source verification key" @@ -124,10 +610,83 @@ api_secret="Private Key" merchant_account_id="Merchant Account Id" merchant_config_currency="Currency" +[cashtocode] +[[cashtocode.reward]] + payment_method_type = "classic" +[[cashtocode.reward]] + payment_method_type = "evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" + + +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" + [checkout] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay","paypal"] +[[checkout.credit]] + payment_method_type = "Mastercard" +[[checkout.credit]] + payment_method_type = "Visa" +[[checkout.credit]] + payment_method_type = "Interac" +[[checkout.credit]] + payment_method_type = "AmericanExpress" +[[checkout.credit]] + payment_method_type = "JCB" +[[checkout.credit]] + payment_method_type = "DinersClub" +[[checkout.credit]] + payment_method_type = "Discover" +[[checkout.credit]] + payment_method_type = "CartesBancaires" +[[checkout.credit]] + payment_method_type = "UnionPay" +[[checkout.debit]] + payment_method_type = "Mastercard" +[[checkout.debit]] + payment_method_type = "Visa" +[[checkout.debit]] + payment_method_type = "Interac" +[[checkout.debit]] + payment_method_type = "AmericanExpress" +[[checkout.debit]] + payment_method_type = "JCB" +[[checkout.debit]] + payment_method_type = "DinersClub" +[[checkout.debit]] + payment_method_type = "Discover" +[[checkout.debit]] + payment_method_type = "CartesBancaires" +[[checkout.debit]] + payment_method_type = "UnionPay" +[[checkout.wallet]] + payment_method_type = "apple_pay" +[[checkout.wallet]] + payment_method_type = "google_pay" +[[checkout.wallet]] + payment_method_type = "paypal" [checkout.connector_auth.SignatureKey] api_key="Checkout API Public Key" key1="Processing Channel ID" @@ -152,19 +711,64 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" - - [coinbase] -crypto = ["crypto_currency"] +[[coinbase.crypto]] + payment_method_type = "crypto_currency" [coinbase.connector_auth.HeaderKey] api_key="API Key" [coinbase.connector_webhook_details] merchant_secret="Source verification key" +[cryptopay] +[[cryptopay.crypto]] + payment_method_type = "crypto_currency" +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + [cybersource] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] +[[cybersource.credit]] + payment_method_type = "Mastercard" +[[cybersource.credit]] + payment_method_type = "Visa" +[[cybersource.credit]] + payment_method_type = "Interac" +[[cybersource.credit]] + payment_method_type = "AmericanExpress" +[[cybersource.credit]] + payment_method_type = "JCB" +[[cybersource.credit]] + payment_method_type = "DinersClub" +[[cybersource.credit]] + payment_method_type = "Discover" +[[cybersource.credit]] + payment_method_type = "CartesBancaires" +[[cybersource.credit]] + payment_method_type = "UnionPay" +[[cybersource.debit]] + payment_method_type = "Mastercard" +[[cybersource.debit]] + payment_method_type = "Visa" +[[cybersource.debit]] + payment_method_type = "Interac" +[[cybersource.debit]] + payment_method_type = "AmericanExpress" +[[cybersource.debit]] + payment_method_type = "JCB" +[[cybersource.debit]] + payment_method_type = "DinersClub" +[[cybersource.debit]] + payment_method_type = "Discover" +[[cybersource.debit]] + payment_method_type = "CartesBancaires" +[[cybersource.debit]] + payment_method_type = "UnionPay" +[[cybersource.wallet]] + payment_method_type = "apple_pay" +[[cybersource.wallet]] + payment_method_type = "google_pay" [cybersource.connector_auth.SignatureKey] api_key="Key" key1="Merchant ID" @@ -189,63 +793,43 @@ merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" -[iatapay] -upi=["upi_collect"] -[iatapay.connector_auth.SignatureKey] -api_key="Client ID" -key1="Airline ID" -api_secret="Client Secret" -[iatapay.connector_webhook_details] -merchant_secret="Source verification key" - -[opennode] -crypto = ["crypto_currency"] -[opennode.connector_auth.HeaderKey] -api_key="API Key" -[opennode.connector_webhook_details] -merchant_secret="Source verification key" - -[prophetpay] -card_redirect = ["card_redirect"] -[prophetpay.connector_auth.SignatureKey] -api_key="Username" -key1="Token" -api_secret="Profile" - - -[bambora] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","paypal"] -[bambora.connector_auth.BodyKey] -api_key="Passcode" -key1="Merchant Id" -[bambora.connector_webhook_details] -merchant_secret="Source verification key" - -[bambora.metadata.apple_pay.session_token_data] -certificate="Merchant Certificate (Base64 Encoded)" -certificate_keys="Merchant PrivateKey (Base64 Encoded)" -merchant_identifier="Apple Merchant Identifier" -display_name="Display Name" -initiative="Domain" -initiative_context="Domain Name" -[bambora.metadata.apple_pay.payment_request_data] -supported_networks=["visa","masterCard","amex","discover"] -merchant_capabilities=["supports3DS"] -label="apple" - -[boku] -wallet = ["dana","gcash","go_pay","kakao_pay","momo"] -[boku.connector_auth.BodyKey] -api_key="API KEY" -key1= "MERCHANT ID" -[boku.connector_webhook_details] -merchant_secret="Source verification key" - [dlocal] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[dlocal.credit]] + payment_method_type = "Mastercard" +[[dlocal.credit]] + payment_method_type = "Visa" +[[dlocal.credit]] + payment_method_type = "Interac" +[[dlocal.credit]] + payment_method_type = "AmericanExpress" +[[dlocal.credit]] + payment_method_type = "JCB" +[[dlocal.credit]] + payment_method_type = "DinersClub" +[[dlocal.credit]] + payment_method_type = "Discover" +[[dlocal.credit]] + payment_method_type = "CartesBancaires" +[[dlocal.credit]] + payment_method_type = "UnionPay" +[[dlocal.debit]] + payment_method_type = "Mastercard" +[[dlocal.debit]] + payment_method_type = "Visa" +[[dlocal.debit]] + payment_method_type = "Interac" +[[dlocal.debit]] + payment_method_type = "AmericanExpress" +[[dlocal.debit]] + payment_method_type = "JCB" +[[dlocal.debit]] + payment_method_type = "DinersClub" +[[dlocal.debit]] + payment_method_type = "Discover" +[[dlocal.debit]] + payment_method_type = "CartesBancaires" +[[dlocal.debit]] + payment_method_type = "UnionPay" [dlocal.connector_auth.SignatureKey] api_key="X Login" key1="X Trans Key" @@ -254,8 +838,42 @@ api_secret="Secret Key" merchant_secret="Source verification key" [fiserv] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[fiserv.credit]] + payment_method_type = "Mastercard" +[[fiserv.credit]] + payment_method_type = "Visa" +[[fiserv.credit]] + payment_method_type = "Interac" +[[fiserv.credit]] + payment_method_type = "AmericanExpress" +[[fiserv.credit]] + payment_method_type = "JCB" +[[fiserv.credit]] + payment_method_type = "DinersClub" +[[fiserv.credit]] + payment_method_type = "Discover" +[[fiserv.credit]] + payment_method_type = "CartesBancaires" +[[fiserv.credit]] + payment_method_type = "UnionPay" +[[fiserv.debit]] + payment_method_type = "Mastercard" +[[fiserv.debit]] + payment_method_type = "Visa" +[[fiserv.debit]] + payment_method_type = "Interac" +[[fiserv.debit]] + payment_method_type = "AmericanExpress" +[[fiserv.debit]] + payment_method_type = "JCB" +[[fiserv.debit]] + payment_method_type = "DinersClub" +[[fiserv.debit]] + payment_method_type = "Discover" +[[fiserv.debit]] + payment_method_type = "CartesBancaires" +[[fiserv.debit]] + payment_method_type = "UnionPay" [fiserv.connector_auth.SignatureKey] api_key="API Key" key1="Merchant ID" @@ -266,8 +884,42 @@ terminal_id="Terminal ID" merchant_secret="Source verification key" [forte] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[forte.credit]] + payment_method_type = "Mastercard" +[[forte.credit]] + payment_method_type = "Visa" +[[forte.credit]] + payment_method_type = "Interac" +[[forte.credit]] + payment_method_type = "AmericanExpress" +[[forte.credit]] + payment_method_type = "JCB" +[[forte.credit]] + payment_method_type = "DinersClub" +[[forte.credit]] + payment_method_type = "Discover" +[[forte.credit]] + payment_method_type = "CartesBancaires" +[[forte.credit]] + payment_method_type = "UnionPay" +[[forte.debit]] + payment_method_type = "Mastercard" +[[forte.debit]] + payment_method_type = "Visa" +[[forte.debit]] + payment_method_type = "Interac" +[[forte.debit]] + payment_method_type = "AmericanExpress" +[[forte.debit]] + payment_method_type = "JCB" +[[forte.debit]] + payment_method_type = "DinersClub" +[[forte.debit]] + payment_method_type = "Discover" +[[forte.debit]] + payment_method_type = "CartesBancaires" +[[forte.debit]] + payment_method_type = "UnionPay" [forte.connector_auth.MultiAuthKey] api_key="API Access ID" key1="Organization ID" @@ -277,10 +929,54 @@ key2="Location ID" merchant_secret="Source verification key" [globalpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["google_pay","paypal"] +[[globalpay.credit]] + payment_method_type = "Mastercard" +[[globalpay.credit]] + payment_method_type = "Visa" +[[globalpay.credit]] + payment_method_type = "Interac" +[[globalpay.credit]] + payment_method_type = "AmericanExpress" +[[globalpay.credit]] + payment_method_type = "JCB" +[[globalpay.credit]] + payment_method_type = "DinersClub" +[[globalpay.credit]] + payment_method_type = "Discover" +[[globalpay.credit]] + payment_method_type = "CartesBancaires" +[[globalpay.credit]] + payment_method_type = "UnionPay" +[[globalpay.debit]] + payment_method_type = "Mastercard" +[[globalpay.debit]] + payment_method_type = "Visa" +[[globalpay.debit]] + payment_method_type = "Interac" +[[globalpay.debit]] + payment_method_type = "AmericanExpress" +[[globalpay.debit]] + payment_method_type = "JCB" +[[globalpay.debit]] + payment_method_type = "DinersClub" +[[globalpay.debit]] + payment_method_type = "Discover" +[[globalpay.debit]] + payment_method_type = "CartesBancaires" +[[globalpay.debit]] + payment_method_type = "UnionPay" +[[globalpay.bank_redirect]] + payment_method_type = "ideal" +[[globalpay.bank_redirect]] + payment_method_type = "giropay" +[[globalpay.bank_redirect]] + payment_method_type = "sofort" +[[globalpay.bank_redirect]] + payment_method_type = "eps" +[[globalpay.wallet]] + payment_method_type = "google_pay" +[[globalpay.wallet]] + payment_method_type = "paypal" [globalpay.connector_auth.BodyKey] api_key="Global App Key" key1="Global App ID" @@ -288,24 +984,103 @@ key1="Global App ID" account_name="Account Name" [globalpay.connector_webhook_details] merchant_secret="Source verification key" - [globalpay.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" +[globepay] +[[globepay.wallet]] + payment_method_type = "we_chat_pay" +[[globepay.wallet]] + payment_method_type = "ali_pay" +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[gocardless] +[[gocardless.bank_debit]] + payment_method_type = "ach" +[[gocardless.bank_debit]] + payment_method_type = "becs" +[[gocardless.bank_debit]] + payment_method_type = "sepa" +[gocardless.connector_auth.HeaderKey] +api_key="Access Token" +[gocardless.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +[[iatapay.upi]] + payment_method_type = "upi_collect" +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + [klarna] -pay_later=["klarna"] +[[klarna.pay_later]] + payment_method_type = "klarna" [klarna.connector_auth.HeaderKey] api_key="Klarna API Key" [klarna.connector_webhook_details] merchant_secret="Source verification key" [mollie] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps","przelewy24","bancontact_card"] -wallet = ["paypal"] +[[mollie.credit]] + payment_method_type = "Mastercard" +[[mollie.credit]] + payment_method_type = "Visa" +[[mollie.credit]] + payment_method_type = "Interac" +[[mollie.credit]] + payment_method_type = "AmericanExpress" +[[mollie.credit]] + payment_method_type = "JCB" +[[mollie.credit]] + payment_method_type = "DinersClub" +[[mollie.credit]] + payment_method_type = "Discover" +[[mollie.credit]] + payment_method_type = "CartesBancaires" +[[mollie.credit]] + payment_method_type = "UnionPay" +[[mollie.debit]] + payment_method_type = "Mastercard" +[[mollie.debit]] + payment_method_type = "Visa" +[[mollie.debit]] + payment_method_type = "Interac" +[[mollie.debit]] + payment_method_type = "AmericanExpress" +[[mollie.debit]] + payment_method_type = "JCB" +[[mollie.debit]] + payment_method_type = "DinersClub" +[[mollie.debit]] + payment_method_type = "Discover" +[[mollie.debit]] + payment_method_type = "CartesBancaires" +[[mollie.debit]] + payment_method_type = "UnionPay" +[[mollie.bank_redirect]] + payment_method_type = "ideal" +[[mollie.bank_redirect]] + payment_method_type = "giropay" +[[mollie.bank_redirect]] + payment_method_type = "sofort" +[[mollie.bank_redirect]] + payment_method_type = "eps" +[[mollie.bank_redirect]] + payment_method_type = "przelewy24" +[[mollie.bank_redirect]] + payment_method_type = "bancontact_card" +[[mollie.wallet]] + payment_method_type = "paypal" [mollie.connector_auth.BodyKey] api_key="API Key" key1="Profile Token" @@ -313,30 +1088,109 @@ key1="Profile Token" merchant_secret="Source verification key" [multisafepay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","paypal"] +[[multisafepay.credit]] + payment_method_type = "Mastercard" +[[multisafepay.credit]] + payment_method_type = "Visa" +[[multisafepay.credit]] + payment_method_type = "Interac" +[[multisafepay.credit]] + payment_method_type = "AmericanExpress" +[[multisafepay.credit]] + payment_method_type = "JCB" +[[multisafepay.credit]] + payment_method_type = "DinersClub" +[[multisafepay.credit]] + payment_method_type = "Discover" +[[multisafepay.credit]] + payment_method_type = "CartesBancaires" +[[multisafepay.credit]] + payment_method_type = "UnionPay" +[[multisafepay.debit]] + payment_method_type = "Mastercard" +[[multisafepay.debit]] + payment_method_type = "Visa" +[[multisafepay.debit]] + payment_method_type = "Interac" +[[multisafepay.debit]] + payment_method_type = "AmericanExpress" +[[multisafepay.debit]] + payment_method_type = "JCB" +[[multisafepay.debit]] + payment_method_type = "DinersClub" +[[multisafepay.debit]] + payment_method_type = "Discover" +[[multisafepay.debit]] + payment_method_type = "CartesBancaires" +[[multisafepay.debit]] + payment_method_type = "UnionPay" +[[multisafepay.wallet]] + payment_method_type = "google_pay" +[[multisafepay.wallet]] + payment_method_type = "paypal" [multisafepay.connector_auth.HeaderKey] api_key="Enter API Key" [multisafepay.connector_webhook_details] merchant_secret="Source verification key" - [multisafepay.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" [nexinets] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["apple_pay","paypal"] +[[nexinets.credit]] + payment_method_type = "Mastercard" +[[nexinets.credit]] + payment_method_type = "Visa" +[[nexinets.credit]] + payment_method_type = "Interac" +[[nexinets.credit]] + payment_method_type = "AmericanExpress" +[[nexinets.credit]] + payment_method_type = "JCB" +[[nexinets.credit]] + payment_method_type = "DinersClub" +[[nexinets.credit]] + payment_method_type = "Discover" +[[nexinets.credit]] + payment_method_type = "CartesBancaires" +[[nexinets.credit]] + payment_method_type = "UnionPay" +[[nexinets.debit]] + payment_method_type = "Mastercard" +[[nexinets.debit]] + payment_method_type = "Visa" +[[nexinets.debit]] + payment_method_type = "Interac" +[[nexinets.debit]] + payment_method_type = "AmericanExpress" +[[nexinets.debit]] + payment_method_type = "JCB" +[[nexinets.debit]] + payment_method_type = "DinersClub" +[[nexinets.debit]] + payment_method_type = "Discover" +[[nexinets.debit]] + payment_method_type = "CartesBancaires" +[[nexinets.debit]] + payment_method_type = "UnionPay" +[[nexinets.bank_redirect]] + payment_method_type = "ideal" +[[nexinets.bank_redirect]] + payment_method_type = "giropay" +[[nexinets.bank_redirect]] + payment_method_type = "sofort" +[[nexinets.bank_redirect]] + payment_method_type = "eps" +[[nexinets.wallet]] + payment_method_type = "apple_pay" +[[nexinets.wallet]] + payment_method_type = "paypal" [nexinets.connector_auth.BodyKey] api_key="API Key" key1="Merchant ID" [nexinets.connector_webhook_details] merchant_secret="Source verification key" - [nexinets.metadata.apple_pay.session_token_data] certificate="Merchant Certificate (Base64 Encoded)" certificate_keys="Merchant PrivateKey (Base64 Encoded)" @@ -350,9 +1204,48 @@ merchant_capabilities=["supports3DS"] label="apple" [nmi] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] +[[nmi.credit]] + payment_method_type = "Mastercard" +[[nmi.credit]] + payment_method_type = "Visa" +[[nmi.credit]] + payment_method_type = "Interac" +[[nmi.credit]] + payment_method_type = "AmericanExpress" +[[nmi.credit]] + payment_method_type = "JCB" +[[nmi.credit]] + payment_method_type = "DinersClub" +[[nmi.credit]] + payment_method_type = "Discover" +[[nmi.credit]] + payment_method_type = "CartesBancaires" +[[nmi.credit]] + payment_method_type = "UnionPay" +[[nmi.debit]] + payment_method_type = "Mastercard" +[[nmi.debit]] + payment_method_type = "Visa" +[[nmi.debit]] + payment_method_type = "Interac" +[[nmi.debit]] + payment_method_type = "AmericanExpress" +[[nmi.debit]] + payment_method_type = "JCB" +[[nmi.debit]] + payment_method_type = "DinersClub" +[[nmi.debit]] + payment_method_type = "Discover" +[[nmi.debit]] + payment_method_type = "CartesBancaires" +[[nmi.debit]] + payment_method_type = "UnionPay" +[[nmi.bank_redirect]] + payment_method_type = "ideal" +[[nmi.wallet]] + payment_method_type = "apple_pay" +[[nmi.wallet]] + payment_method_type = "google_pay" [nmi.connector_auth.HeaderKey] api_key="API Key" [nmi.connector_webhook_details] @@ -376,9 +1269,48 @@ merchant_capabilities=["supports3DS"] label="apple" [noon] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay","paypal"] +[[noon.credit]] + payment_method_type = "Mastercard" +[[noon.credit]] + payment_method_type = "Visa" +[[noon.credit]] + payment_method_type = "Interac" +[[noon.credit]] + payment_method_type = "AmericanExpress" +[[noon.credit]] + payment_method_type = "JCB" +[[noon.credit]] + payment_method_type = "DinersClub" +[[noon.credit]] + payment_method_type = "Discover" +[[noon.credit]] + payment_method_type = "CartesBancaires" +[[noon.credit]] + payment_method_type = "UnionPay" +[[noon.debit]] + payment_method_type = "Mastercard" +[[noon.debit]] + payment_method_type = "Visa" +[[noon.debit]] + payment_method_type = "Interac" +[[noon.debit]] + payment_method_type = "AmericanExpress" +[[noon.debit]] + payment_method_type = "JCB" +[[noon.debit]] + payment_method_type = "DinersClub" +[[noon.debit]] + payment_method_type = "Discover" +[[noon.debit]] + payment_method_type = "CartesBancaires" +[[noon.debit]] + payment_method_type = "UnionPay" +[[noon.wallet]] + payment_method_type = "apple_pay" +[[noon.wallet]] + payment_method_type = "google_pay" +[[noon.wallet]] + payment_method_type = "paypal" [noon.connector_auth.SignatureKey] api_key="API Key" key1="Business Identifier" @@ -404,11 +1336,60 @@ merchant_capabilities=["supports3DS"] label="apple" [nuvei] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","afterpay_clearpay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["apple_pay","google_pay","paypal"] +[[nuvei.credit]] + payment_method_type = "Mastercard" +[[nuvei.credit]] + payment_method_type = "Visa" +[[nuvei.credit]] + payment_method_type = "Interac" +[[nuvei.credit]] + payment_method_type = "AmericanExpress" +[[nuvei.credit]] + payment_method_type = "JCB" +[[nuvei.credit]] + payment_method_type = "DinersClub" +[[nuvei.credit]] + payment_method_type = "Discover" +[[nuvei.credit]] + payment_method_type = "CartesBancaires" +[[nuvei.credit]] + payment_method_type = "UnionPay" +[[nuvei.debit]] + payment_method_type = "Mastercard" +[[nuvei.debit]] + payment_method_type = "Visa" +[[nuvei.debit]] + payment_method_type = "Interac" +[[nuvei.debit]] + payment_method_type = "AmericanExpress" +[[nuvei.debit]] + payment_method_type = "JCB" +[[nuvei.debit]] + payment_method_type = "DinersClub" +[[nuvei.debit]] + payment_method_type = "Discover" +[[nuvei.debit]] + payment_method_type = "CartesBancaires" +[[nuvei.debit]] + payment_method_type = "UnionPay" +[[nuvei.pay_later]] + payment_method_type = "klarna" +[[nuvei.pay_later]] + payment_method_type = "afterpay_clearpay" +[[nuvei.bank_redirect]] + payment_method_type = "ideal" +[[nuvei.bank_redirect]] + payment_method_type = "giropay" +[[nuvei.bank_redirect]] + payment_method_type = "sofort" +[[nuvei.bank_redirect]] + payment_method_type = "eps" +[[nuvei.wallet]] + payment_method_type = "apple_pay" +[[nuvei.wallet]] + payment_method_type = "google_pay" +[[nuvei.wallet]] + payment_method_type = "paypal" [nuvei.connector_auth.SignatureKey] api_key="Merchant ID" key1="Merchant Site ID" @@ -433,11 +1414,114 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" + +[opennode] +[[opennode.crypto]] + payment_method_type = "crypto_currency" +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[prophetpay] +[[prophetpay.card_redirect]] + payment_method_type = "card_redirect" +[prophetpay.connector_auth.SignatureKey] +api_key="Username" +key1="Token" +api_secret="Profile" + +[payme] +[[payme.credit]] + payment_method_type = "Mastercard" +[[payme.credit]] + payment_method_type = "Visa" +[[payme.credit]] + payment_method_type = "Interac" +[[payme.credit]] + payment_method_type = "AmericanExpress" +[[payme.credit]] + payment_method_type = "JCB" +[[payme.credit]] + payment_method_type = "DinersClub" +[[payme.credit]] + payment_method_type = "Discover" +[[payme.credit]] + payment_method_type = "CartesBancaires" +[[payme.credit]] + payment_method_type = "UnionPay" +[[payme.debit]] + payment_method_type = "Mastercard" +[[payme.debit]] + payment_method_type = "Visa" +[[payme.debit]] + payment_method_type = "Interac" +[[payme.debit]] + payment_method_type = "AmericanExpress" +[[payme.debit]] + payment_method_type = "JCB" +[[payme.debit]] + payment_method_type = "DinersClub" +[[payme.debit]] + payment_method_type = "Discover" +[[payme.debit]] + payment_method_type = "CartesBancaires" +[[payme.debit]] + payment_method_type = "UnionPay" +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + [paypal] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["paypal"] -bank_redirect=["ideal","giropay","sofort","eps"] +[[paypal.credit]] + payment_method_type = "Mastercard" +[[paypal.credit]] + payment_method_type = "Visa" +[[paypal.credit]] + payment_method_type = "Interac" +[[paypal.credit]] + payment_method_type = "AmericanExpress" +[[paypal.credit]] + payment_method_type = "JCB" +[[paypal.credit]] + payment_method_type = "DinersClub" +[[paypal.credit]] + payment_method_type = "Discover" +[[paypal.credit]] + payment_method_type = "CartesBancaires" +[[paypal.credit]] + payment_method_type = "UnionPay" +[[paypal.debit]] + payment_method_type = "Mastercard" +[[paypal.debit]] + payment_method_type = "Visa" +[[paypal.debit]] + payment_method_type = "Interac" +[[paypal.debit]] + payment_method_type = "AmericanExpress" +[[paypal.debit]] + payment_method_type = "JCB" +[[paypal.debit]] + payment_method_type = "DinersClub" +[[paypal.debit]] + payment_method_type = "Discover" +[[paypal.debit]] + payment_method_type = "CartesBancaires" +[[paypal.debit]] + payment_method_type = "UnionPay" +[[paypal.wallet]] + payment_method_type = "paypal" +[[paypal.bank_redirect]] + payment_method_type = "ideal" +[[paypal.bank_redirect]] + payment_method_type = "giropay" +[[paypal.bank_redirect]] + payment_method_type = "sofort" +[[paypal.bank_redirect]] + payment_method_type = "eps" is_verifiable = true [paypal.connector_auth.BodyKey] api_key="Client Secret" @@ -445,10 +1529,46 @@ key1="Client ID" [paypal.connector_webhook_details] merchant_secret="Source verification key" + [payu] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay"] +[[payu.credit]] + payment_method_type = "Mastercard" +[[payu.credit]] + payment_method_type = "Visa" +[[payu.credit]] + payment_method_type = "Interac" +[[payu.credit]] + payment_method_type = "AmericanExpress" +[[payu.credit]] + payment_method_type = "JCB" +[[payu.credit]] + payment_method_type = "DinersClub" +[[payu.credit]] + payment_method_type = "Discover" +[[payu.credit]] + payment_method_type = "CartesBancaires" +[[payu.credit]] + payment_method_type = "UnionPay" +[[payu.debit]] + payment_method_type = "Mastercard" +[[payu.debit]] + payment_method_type = "Visa" +[[payu.debit]] + payment_method_type = "Interac" +[[payu.debit]] + payment_method_type = "AmericanExpress" +[[payu.debit]] + payment_method_type = "JCB" +[[payu.debit]] + payment_method_type = "DinersClub" +[[payu.debit]] + payment_method_type = "Discover" +[[payu.debit]] + payment_method_type = "CartesBancaires" +[[payu.debit]] + payment_method_type = "UnionPay" +[[payu.wallet]] + payment_method_type = "google_pay" [payu.connector_auth.BodyKey] api_key="API Key" key1="Merchant POS ID" @@ -460,10 +1580,129 @@ merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" +[placetopay] +[[placetopay.credit]] + payment_method_type = "Mastercard" +[[placetopay.credit]] + payment_method_type = "Visa" +[[placetopay.credit]] + payment_method_type = "Interac" +[[placetopay.credit]] + payment_method_type = "AmericanExpress" +[[placetopay.credit]] + payment_method_type = "JCB" +[[placetopay.credit]] + payment_method_type = "DinersClub" +[[placetopay.credit]] + payment_method_type = "Discover" +[[placetopay.credit]] + payment_method_type = "CartesBancaires" +[[placetopay.credit]] + payment_method_type = "UnionPay" +[[placetopay.debit]] + payment_method_type = "Mastercard" +[[placetopay.debit]] + payment_method_type = "Visa" +[[placetopay.debit]] + payment_method_type = "Interac" +[[placetopay.debit]] + payment_method_type = "AmericanExpress" +[[placetopay.debit]] + payment_method_type = "JCB" +[[placetopay.debit]] + payment_method_type = "DinersClub" +[[placetopay.debit]] + payment_method_type = "Discover" +[[placetopay.debit]] + payment_method_type = "CartesBancaires" +[[placetopay.debit]] + payment_method_type = "UnionPay" +[placetopay.connector_auth.BodyKey] +api_key="Login" +key1="Trankey" + +[powertranz] +[[powertranz.credit]] + payment_method_type = "Mastercard" +[[powertranz.credit]] + payment_method_type = "Visa" +[[powertranz.credit]] + payment_method_type = "Interac" +[[powertranz.credit]] + payment_method_type = "AmericanExpress" +[[powertranz.credit]] + payment_method_type = "JCB" +[[powertranz.credit]] + payment_method_type = "DinersClub" +[[powertranz.credit]] + payment_method_type = "Discover" +[[powertranz.credit]] + payment_method_type = "CartesBancaires" +[[powertranz.credit]] + payment_method_type = "UnionPay" +[[powertranz.debit]] + payment_method_type = "Mastercard" +[[powertranz.debit]] + payment_method_type = "Visa" +[[powertranz.debit]] + payment_method_type = "Interac" +[[powertranz.debit]] + payment_method_type = "AmericanExpress" +[[powertranz.debit]] + payment_method_type = "JCB" +[[powertranz.debit]] + payment_method_type = "DinersClub" +[[powertranz.debit]] + payment_method_type = "Discover" +[[powertranz.debit]] + payment_method_type = "CartesBancaires" +[[powertranz.debit]] + payment_method_type = "UnionPay" +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" + [rapyd] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay"] +[[rapyd.credit]] + payment_method_type = "Mastercard" +[[rapyd.credit]] + payment_method_type = "Visa" +[[rapyd.credit]] + payment_method_type = "Interac" +[[rapyd.credit]] + payment_method_type = "AmericanExpress" +[[rapyd.credit]] + payment_method_type = "JCB" +[[rapyd.credit]] + payment_method_type = "DinersClub" +[[rapyd.credit]] + payment_method_type = "Discover" +[[rapyd.credit]] + payment_method_type = "CartesBancaires" +[[rapyd.credit]] + payment_method_type = "UnionPay" +[[rapyd.debit]] + payment_method_type = "Mastercard" +[[rapyd.debit]] + payment_method_type = "Visa" +[[rapyd.debit]] + payment_method_type = "Interac" +[[rapyd.debit]] + payment_method_type = "AmericanExpress" +[[rapyd.debit]] + payment_method_type = "JCB" +[[rapyd.debit]] + payment_method_type = "DinersClub" +[[rapyd.debit]] + payment_method_type = "Discover" +[[rapyd.debit]] + payment_method_type = "CartesBancaires" +[[rapyd.debit]] + payment_method_type = "UnionPay" +[[rapyd.wallet]] + payment_method_type = "apple_pay" [rapyd.connector_auth.BodyKey] api_key="Access Key" key1="API Secret" @@ -483,22 +1722,136 @@ merchant_capabilities=["supports3DS"] label="apple" [shift4] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] +[[shift4.credit]] + payment_method_type = "Mastercard" +[[shift4.credit]] + payment_method_type = "Visa" +[[shift4.credit]] + payment_method_type = "Interac" +[[shift4.credit]] + payment_method_type = "AmericanExpress" +[[shift4.credit]] + payment_method_type = "JCB" +[[shift4.credit]] + payment_method_type = "DinersClub" +[[shift4.credit]] + payment_method_type = "Discover" +[[shift4.credit]] + payment_method_type = "CartesBancaires" +[[shift4.credit]] + payment_method_type = "UnionPay" +[[shift4.debit]] + payment_method_type = "Mastercard" +[[shift4.debit]] + payment_method_type = "Visa" +[[shift4.debit]] + payment_method_type = "Interac" +[[shift4.debit]] + payment_method_type = "AmericanExpress" +[[shift4.debit]] + payment_method_type = "JCB" +[[shift4.debit]] + payment_method_type = "DinersClub" +[[shift4.debit]] + payment_method_type = "Discover" +[[shift4.debit]] + payment_method_type = "CartesBancaires" +[[shift4.debit]] + payment_method_type = "UnionPay" +[[shift4.bank_redirect]] + payment_method_type = "ideal" +[[shift4.bank_redirect]] + payment_method_type = "giropay" +[[shift4.bank_redirect]] + payment_method_type = "sofort" +[[shift4.bank_redirect]] + payment_method_type = "eps" [shift4.connector_auth.HeaderKey] api_key="API Key" [shift4.connector_webhook_details] merchant_secret="Source verification key" [stripe] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","affirm","afterpay_clearpay"] -bank_redirect=["ideal","giropay","sofort","eps","bancontact_card","przelewy24"] -bank_debit=["ach","bacs","becs","sepa"] -bank_transfer=["ach","bacs","sepa", "multibanco"] -wallet = ["apple_pay","google_pay","we_chat_pay","ali_pay", "cashapp"] +[[stripe.credit]] + payment_method_type = "Mastercard" +[[stripe.credit]] + payment_method_type = "Visa" +[[stripe.credit]] + payment_method_type = "Interac" +[[stripe.credit]] + payment_method_type = "AmericanExpress" +[[stripe.credit]] + payment_method_type = "JCB" +[[stripe.credit]] + payment_method_type = "DinersClub" +[[stripe.credit]] + payment_method_type = "Discover" +[[stripe.credit]] + payment_method_type = "CartesBancaires" +[[stripe.credit]] + payment_method_type = "UnionPay" +[[stripe.debit]] + payment_method_type = "Mastercard" +[[stripe.debit]] + payment_method_type = "Visa" +[[stripe.debit]] + payment_method_type = "Interac" +[[stripe.debit]] + payment_method_type = "AmericanExpress" +[[stripe.debit]] + payment_method_type = "JCB" +[[stripe.debit]] + payment_method_type = "DinersClub" +[[stripe.debit]] + payment_method_type = "Discover" +[[stripe.debit]] + payment_method_type = "CartesBancaires" +[[stripe.debit]] + payment_method_type = "UnionPay" +[[stripe.pay_later]] + payment_method_type = "klarna" +[[stripe.pay_later]] + payment_method_type = "affirm" +[[stripe.pay_later]] + payment_method_type = "afterpay_clearpay" +[[stripe.bank_redirect]] + payment_method_type = "ideal" +[[stripe.bank_redirect]] + payment_method_type = "giropay" +[[stripe.bank_redirect]] + payment_method_type = "sofort" +[[stripe.bank_redirect]] + payment_method_type = "eps" +[[stripe.bank_redirect]] + payment_method_type = "bancontact_card" +[[stripe.bank_redirect]] + payment_method_type = "przelewy24" +[[stripe.bank_debit]] + payment_method_type = "ach" +[[stripe.bank_debit]] + payment_method_type = "bacs" +[[stripe.bank_debit]] + payment_method_type = "becs" +[[stripe.bank_debit]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "ach" +[[stripe.bank_transfer]] + payment_method_type = "bacs" +[[stripe.bank_transfer]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "multibanco" +[[stripe.wallet]] + payment_method_type = "apple_pay" +[[stripe.wallet]] + payment_method_type = "google_pay" +[[stripe.wallet]] + payment_method_type = "we_chat_pay" +[[stripe.wallet]] + payment_method_type = "ali_pay" +[[stripe.wallet]] + payment_method_type = "cashapp" is_verifiable = true [stripe.connector_auth.HeaderKey] api_key="Secret Key" @@ -522,29 +1875,144 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" -[zen] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -voucher = ["boleto", "efecty", "pago_efectivo", "red_compra", "red_pagos"] -bank_transfer = ["pix", "pse"] -wallet = ["apple_pay","google_pay"] -[zen.connector_auth.HeaderKey] -api_key="API Key" -[zen.connector_webhook_details] +[stax] +[[stax.credit]] + payment_method_type = "Mastercard" +[[stax.credit]] + payment_method_type = "Visa" +[[stax.credit]] + payment_method_type = "Interac" +[[stax.credit]] + payment_method_type = "AmericanExpress" +[[stax.credit]] + payment_method_type = "JCB" +[[stax.credit]] + payment_method_type = "DinersClub" +[[stax.credit]] + payment_method_type = "Discover" +[[stax.credit]] + payment_method_type = "CartesBancaires" +[[stax.credit]] + payment_method_type = "UnionPay" +[[stax.debit]] + payment_method_type = "Mastercard" +[[stax.debit]] + payment_method_type = "Visa" +[[stax.debit]] + payment_method_type = "Interac" +[[stax.debit]] + payment_method_type = "AmericanExpress" +[[stax.debit]] + payment_method_type = "JCB" +[[stax.debit]] + payment_method_type = "DinersClub" +[[stax.debit]] + payment_method_type = "Discover" +[[stax.debit]] + payment_method_type = "CartesBancaires" +[[stax.debit]] + payment_method_type = "UnionPay" +[[stax.bank_debit]] + payment_method_type = "ach" +[stax.connector_auth.HeaderKey] +api_key="Api Key" +[stax.connector_webhook_details] merchant_secret="Source verification key" -[zen.metadata.apple_pay] -terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" -[zen.metadata.google_pay] -terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" +[square] +[[square.credit]] + payment_method_type = "Mastercard" +[[square.credit]] + payment_method_type = "Visa" +[[square.credit]] + payment_method_type = "Interac" +[[square.credit]] + payment_method_type = "AmericanExpress" +[[square.credit]] + payment_method_type = "JCB" +[[square.credit]] + payment_method_type = "DinersClub" +[[square.credit]] + payment_method_type = "Discover" +[[square.credit]] + payment_method_type = "CartesBancaires" +[[square.credit]] + payment_method_type = "UnionPay" +[[square.debit]] + payment_method_type = "Mastercard" +[[square.debit]] + payment_method_type = "Visa" +[[square.debit]] + payment_method_type = "Interac" +[[square.debit]] + payment_method_type = "AmericanExpress" +[[square.debit]] + payment_method_type = "JCB" +[[square.debit]] + payment_method_type = "DinersClub" +[[square.debit]] + payment_method_type = "Discover" +[[square.debit]] + payment_method_type = "CartesBancaires" +[[square.debit]] + payment_method_type = "UnionPay" +[square_payout.connector_auth.BodyKey] +api_key = "Square API Key" +key1 = "Square Client Id" +[square.connector_webhook_details] +merchant_secret="Source verification key" [trustpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps","blik"] -wallet = ["apple_pay","google_pay"] +[[trustpay.credit]] + payment_method_type = "Mastercard" +[[trustpay.credit]] + payment_method_type = "Visa" +[[trustpay.credit]] + payment_method_type = "Interac" +[[trustpay.credit]] + payment_method_type = "AmericanExpress" +[[trustpay.credit]] + payment_method_type = "JCB" +[[trustpay.credit]] + payment_method_type = "DinersClub" +[[trustpay.credit]] + payment_method_type = "Discover" +[[trustpay.credit]] + payment_method_type = "CartesBancaires" +[[trustpay.credit]] + payment_method_type = "UnionPay" +[[trustpay.debit]] + payment_method_type = "Mastercard" +[[trustpay.debit]] + payment_method_type = "Visa" +[[trustpay.debit]] + payment_method_type = "Interac" +[[trustpay.debit]] + payment_method_type = "AmericanExpress" +[[trustpay.debit]] + payment_method_type = "JCB" +[[trustpay.debit]] + payment_method_type = "DinersClub" +[[trustpay.debit]] + payment_method_type = "Discover" +[[trustpay.debit]] + payment_method_type = "CartesBancaires" +[[trustpay.debit]] + payment_method_type = "UnionPay" +[[trustpay.bank_redirect]] + payment_method_type = "ideal" +[[trustpay.bank_redirect]] + payment_method_type = "giropay" +[[trustpay.bank_redirect]] + payment_method_type = "sofort" +[[trustpay.bank_redirect]] + payment_method_type = "eps" +[[trustpay.bank_redirect]] + payment_method_type = "blik" +[[trustpay.wallet]] + payment_method_type = "apple_pay" +[[trustpay.wallet]] + payment_method_type = "google_pay" [trustpay.connector_auth.SignatureKey] api_key="API Key" key1="Project ID" @@ -564,10 +2032,100 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" +[tsys] +[[tsys.credit]] + payment_method_type = "Mastercard" +[[tsys.credit]] + payment_method_type = "Visa" +[[tsys.credit]] + payment_method_type = "Interac" +[[tsys.credit]] + payment_method_type = "AmericanExpress" +[[tsys.credit]] + payment_method_type = "JCB" +[[tsys.credit]] + payment_method_type = "DinersClub" +[[tsys.credit]] + payment_method_type = "Discover" +[[tsys.credit]] + payment_method_type = "CartesBancaires" +[[tsys.credit]] + payment_method_type = "UnionPay" +[[tsys.debit]] + payment_method_type = "Mastercard" +[[tsys.debit]] + payment_method_type = "Visa" +[[tsys.debit]] + payment_method_type = "Interac" +[[tsys.debit]] + payment_method_type = "AmericanExpress" +[[tsys.debit]] + payment_method_type = "JCB" +[[tsys.debit]] + payment_method_type = "DinersClub" +[[tsys.debit]] + payment_method_type = "Discover" +[[tsys.debit]] + payment_method_type = "CartesBancaires" +[[tsys.debit]] + payment_method_type = "UnionPay" +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[volt] +[[volt.bank_redirect]] + payment_method_type = "open_banking_uk" +[volt.connector_auth.MultiAuthKey] +api_key = "Username" +api_secret = "Password" +key1 = "Client ID" +key2 = "Client Secret" + [worldline] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay"] +[[worldline.credit]] + payment_method_type = "Mastercard" +[[worldline.credit]] + payment_method_type = "Visa" +[[worldline.credit]] + payment_method_type = "Interac" +[[worldline.credit]] + payment_method_type = "AmericanExpress" +[[worldline.credit]] + payment_method_type = "JCB" +[[worldline.credit]] + payment_method_type = "DinersClub" +[[worldline.credit]] + payment_method_type = "Discover" +[[worldline.credit]] + payment_method_type = "CartesBancaires" +[[worldline.credit]] + payment_method_type = "UnionPay" +[[worldline.debit]] + payment_method_type = "Mastercard" +[[worldline.debit]] + payment_method_type = "Visa" +[[worldline.debit]] + payment_method_type = "Interac" +[[worldline.debit]] + payment_method_type = "AmericanExpress" +[[worldline.debit]] + payment_method_type = "JCB" +[[worldline.debit]] + payment_method_type = "DinersClub" +[[worldline.debit]] + payment_method_type = "Discover" +[[worldline.debit]] + payment_method_type = "CartesBancaires" +[[worldline.debit]] + payment_method_type = "UnionPay" +[[worldline.bank_redirect]] + payment_method_type = "ideal" +[[worldline.bank_redirect]] + payment_method_type = "giropay" [worldline.connector_auth.SignatureKey] api_key="API Key ID" key1="Merchant ID" @@ -576,9 +2134,46 @@ api_secret="Secret API Key" merchant_secret="Source verification key" [worldpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","apple_pay"] +[[worldpay.credit]] + payment_method_type = "Mastercard" +[[worldpay.credit]] + payment_method_type = "Visa" +[[worldpay.credit]] + payment_method_type = "Interac" +[[worldpay.credit]] + payment_method_type = "AmericanExpress" +[[worldpay.credit]] + payment_method_type = "JCB" +[[worldpay.credit]] + payment_method_type = "DinersClub" +[[worldpay.credit]] + payment_method_type = "Discover" +[[worldpay.credit]] + payment_method_type = "CartesBancaires" +[[worldpay.credit]] + payment_method_type = "UnionPay" +[[worldpay.debit]] + payment_method_type = "Mastercard" +[[worldpay.debit]] + payment_method_type = "Visa" +[[worldpay.debit]] + payment_method_type = "Interac" +[[worldpay.debit]] + payment_method_type = "AmericanExpress" +[[worldpay.debit]] + payment_method_type = "JCB" +[[worldpay.debit]] + payment_method_type = "DinersClub" +[[worldpay.debit]] + payment_method_type = "Discover" +[[worldpay.debit]] + payment_method_type = "CartesBancaires" +[[worldpay.debit]] + payment_method_type = "UnionPay" +[[worldpay.wallet]] + payment_method_type = "google_pay" +[[worldpay.wallet]] + payment_method_type = "apple_pay" [worldpay.connector_auth.BodyKey] api_key="Username" key1="Password" @@ -602,188 +2197,306 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" -[cashtocode] -reward = ["classic", "evoucher"] -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_webhook_details] -merchant_secret="Source verification key" - -[cryptopay] -crypto = ["crypto_currency"] -[cryptopay.connector_auth.BodyKey] +[zen] +[[zen.credit]] + payment_method_type = "Mastercard" +[[zen.credit]] + payment_method_type = "Visa" +[[zen.credit]] + payment_method_type = "Interac" +[[zen.credit]] + payment_method_type = "AmericanExpress" +[[zen.credit]] + payment_method_type = "JCB" +[[zen.credit]] + payment_method_type = "DinersClub" +[[zen.credit]] + payment_method_type = "Discover" +[[zen.credit]] + payment_method_type = "CartesBancaires" +[[zen.credit]] + payment_method_type = "UnionPay" +[[zen.debit]] + payment_method_type = "Mastercard" +[[zen.debit]] + payment_method_type = "Visa" +[[zen.debit]] + payment_method_type = "Interac" +[[zen.debit]] + payment_method_type = "AmericanExpress" +[[zen.debit]] + payment_method_type = "JCB" +[[zen.debit]] + payment_method_type = "DinersClub" +[[zen.debit]] + payment_method_type = "Discover" +[[zen.debit]] + payment_method_type = "CartesBancaires" +[[zen.debit]] + payment_method_type = "UnionPay" +[[zen.voucher]] + payment_method_type = "boleto" +[[zen.voucher]] + payment_method_type = "efecty" +[[zen.voucher]] + payment_method_type = "pago_efectivo" +[[zen.voucher]] + payment_method_type = "red_compra" +[[zen.voucher]] + payment_method_type = "red_pagos" +[[zen.bank_transfer]] + payment_method_type = "pix" +[[zen.bank_transfer]] + payment_method_type = "pse" +[[zen.wallet]] + payment_method_type = "apple_pay" +[[zen.wallet]] + payment_method_type = "google_pay" +[zen.connector_auth.HeaderKey] api_key="API Key" -key1="Secret Key" -[cryptopay.connector_webhook_details] +[zen.connector_webhook_details] merchant_secret="Source verification key" +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + + [dummy_connector] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[dummy_connector.credit]] + payment_method_type = "Mastercard" +[[dummy_connector.credit]] + payment_method_type = "Visa" +[[dummy_connector.credit]] + payment_method_type = "Interac" +[[dummy_connector.credit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.credit]] + payment_method_type = "JCB" +[[dummy_connector.credit]] + payment_method_type = "DinersClub" +[[dummy_connector.credit]] + payment_method_type = "Discover" +[[dummy_connector.credit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.credit]] + payment_method_type = "UnionPay" +[[dummy_connector.debit]] + payment_method_type = "Mastercard" +[[dummy_connector.debit]] + payment_method_type = "Visa" +[[dummy_connector.debit]] + payment_method_type = "Interac" +[[dummy_connector.debit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.debit]] + payment_method_type = "JCB" +[[dummy_connector.debit]] + payment_method_type = "DinersClub" +[[dummy_connector.debit]] + payment_method_type = "Discover" +[[dummy_connector.debit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.debit]] + payment_method_type = "UnionPay" [dummy_connector.connector_auth.HeaderKey] api_key="Api Key" -[helcim] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[helcim.connector_auth.HeaderKey] +[paypal_test] +[[paypal_test.credit]] + payment_method_type = "Mastercard" +[[paypal_test.credit]] + payment_method_type = "Visa" +[[paypal_test.credit]] + payment_method_type = "Interac" +[[paypal_test.credit]] + payment_method_type = "AmericanExpress" +[[paypal_test.credit]] + payment_method_type = "JCB" +[[paypal_test.credit]] + payment_method_type = "DinersClub" +[[paypal_test.credit]] + payment_method_type = "Discover" +[[paypal_test.credit]] + payment_method_type = "CartesBancaires" +[[paypal_test.credit]] + payment_method_type = "UnionPay" +[[paypal_test.debit]] + payment_method_type = "Mastercard" +[[paypal_test.debit]] + payment_method_type = "Visa" +[[paypal_test.debit]] + payment_method_type = "Interac" +[[paypal_test.debit]] + payment_method_type = "AmericanExpress" +[[paypal_test.debit]] + payment_method_type = "JCB" +[[paypal_test.debit]] + payment_method_type = "DinersClub" +[[paypal_test.debit]] + payment_method_type = "Discover" +[[paypal_test.debit]] + payment_method_type = "CartesBancaires" +[[paypal_test.debit]] + payment_method_type = "UnionPay" +[[paypal_test.wallet]] + payment_method_type = "paypal" +[paypal_test.connector_auth.HeaderKey] api_key="Api Key" -[helcim.connector_webhook_details] -merchant_secret="Source verification key" [stripe_test] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","ali_pay","we_chat_pay"] -pay_later=["klarna","affirm","afterpay_clearpay"] +[[stripe_test.credit]] + payment_method_type = "Mastercard" +[[stripe_test.credit]] + payment_method_type = "Visa" +[[stripe_test.credit]] + payment_method_type = "Interac" +[[stripe_test.credit]] + payment_method_type = "AmericanExpress" +[[stripe_test.credit]] + payment_method_type = "JCB" +[[stripe_test.credit]] + payment_method_type = "DinersClub" +[[stripe_test.credit]] + payment_method_type = "Discover" +[[stripe_test.credit]] + payment_method_type = "CartesBancaires" +[[stripe_test.credit]] + payment_method_type = "UnionPay" +[[stripe_test.debit]] + payment_method_type = "Mastercard" +[[stripe_test.debit]] + payment_method_type = "Visa" +[[stripe_test.debit]] + payment_method_type = "Interac" +[[stripe_test.debit]] + payment_method_type = "AmericanExpress" +[[stripe_test.debit]] + payment_method_type = "JCB" +[[stripe_test.debit]] + payment_method_type = "DinersClub" +[[stripe_test.debit]] + payment_method_type = "Discover" +[[stripe_test.debit]] + payment_method_type = "CartesBancaires" +[[stripe_test.debit]] + payment_method_type = "UnionPay" +[[stripe_test.wallet]] + payment_method_type = "google_pay" +[[stripe_test.wallet]] + payment_method_type = "ali_pay" +[[stripe_test.wallet]] + payment_method_type = "we_chat_pay" +[[stripe_test.pay_later]] + payment_method_type = "klarna" +[[stripe_test.pay_later]] + payment_method_type = "affirm" +[[stripe_test.pay_later]] + payment_method_type = "afterpay_clearpay" [stripe_test.connector_auth.HeaderKey] api_key="Api Key" - -[paypal_test] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["paypal"] -[paypal_test.connector_auth.HeaderKey] +[helcim] +[[helcim.credit]] + payment_method_type = "Mastercard" +[[helcim.credit]] + payment_method_type = "Visa" +[[helcim.credit]] + payment_method_type = "Interac" +[[helcim.credit]] + payment_method_type = "AmericanExpress" +[[helcim.credit]] + payment_method_type = "JCB" +[[helcim.credit]] + payment_method_type = "DinersClub" +[[helcim.credit]] + payment_method_type = "Discover" +[[helcim.credit]] + payment_method_type = "CartesBancaires" +[[helcim.credit]] + payment_method_type = "UnionPay" +[[helcim.debit]] + payment_method_type = "Mastercard" +[[helcim.debit]] + payment_method_type = "Visa" +[[helcim.debit]] + payment_method_type = "Interac" +[[helcim.debit]] + payment_method_type = "AmericanExpress" +[[helcim.debit]] + payment_method_type = "JCB" +[[helcim.debit]] + payment_method_type = "DinersClub" +[[helcim.debit]] + payment_method_type = "Discover" +[[helcim.debit]] + payment_method_type = "CartesBancaires" +[[helcim.debit]] + payment_method_type = "UnionPay" +[helcim.connector_auth.HeaderKey] api_key="Api Key" -[payme] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[payme.connector_auth.BodyKey] -api_key="Seller Payme Id" -key1="Payme Public Key" -[payme.connector_webhook_details] -merchant_secret="Payme Client Secret" -additional_secret="Payme Client Key" - -[powertranz] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[powertranz.connector_auth.BodyKey] -key1 = "PowerTranz Id" -api_key="PowerTranz Password" -[powertranz.connector_webhook_details] -merchant_secret="Source verification key" - -[globepay] -wallet = ["we_chat_pay","ali_pay"] -[globepay.connector_auth.BodyKey] -api_key="Partner Code" -key1="Credential Code" -[globepay.connector_webhook_details] -merchant_secret="Source verification key" - -[tsys] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[tsys.connector_auth.SignatureKey] -api_key="Device Id" -key1="Transaction Key" -api_secret="Developer Id" -[tsys.connector_webhook_details] -merchant_secret="Source verification key" -[square] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[square_payout.connector_auth.BodyKey] -api_key = "Square API Key" -key1 = "Square Client Id" -[square.connector_webhook_details] -merchant_secret="Source verification key" -[stax] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_debit=["ach"] -[stax.connector_auth.HeaderKey] -api_key="Api Key" -[stax.connector_webhook_details] -merchant_secret="Source verification key" - -[volt] -bank_redirect = ["open_banking_uk"] -[volt.connector_auth.MultiAuthKey] -api_key = "Username" -api_secret = "Password" -key1 = "Client ID" -key2 = "Client Secret" - -[wise_payout] -bank_transfer = ["ach","bacs","sepa"] -[wise_payout.connector_auth.BodyKey] -api_key = "Wise API Key" -key1 = "Wise Account Id" [adyen_payout] -bank_transfer = ["ach","bacs","sepa"] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[adyen_payout.credit]] + payment_method_type = "Mastercard" +[[adyen_payout.credit]] + payment_method_type = "Visa" +[[adyen_payout.credit]] + payment_method_type = "Interac" +[[adyen_payout.credit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.credit]] + payment_method_type = "JCB" +[[adyen_payout.credit]] + payment_method_type = "DinersClub" +[[adyen_payout.credit]] + payment_method_type = "Discover" +[[adyen_payout.credit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.credit]] + payment_method_type = "UnionPay" +[[adyen_payout.debit]] + payment_method_type = "Mastercard" +[[adyen_payout.debit]] + payment_method_type = "Visa" +[[adyen_payout.debit]] + payment_method_type = "Interac" +[[adyen_payout.debit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.debit]] + payment_method_type = "JCB" +[[adyen_payout.debit]] + payment_method_type = "DinersClub" +[[adyen_payout.debit]] + payment_method_type = "Discover" +[[adyen_payout.debit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.debit]] + payment_method_type = "UnionPay" +[[adyen_payout.bank_transfer]] + payment_method_type = "ach" +[[adyen_payout.bank_transfer]] + payment_method_type = "bacs" +[[adyen_payout.bank_transfer]] + payment_method_type = "sepa" [adyen_payout.connector_auth.SignatureKey] api_key = "Adyen API Key (Payout creation)" api_secret = "Adyen Key (Payout submission)" key1 = "Adyen Account Id" -[gocardless] -bank_debit=["ach","becs","sepa"] -[gocardless.connector_auth.HeaderKey] -api_key="Access Token" -[gocardless.connector_webhook_details] -merchant_secret="Source verification key" - -[bankofamerica] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] - -[bankofamerica.connector_auth.SignatureKey] -api_key="Key" -key1="Merchant ID" -api_secret="Shared Secret" -[bankofamerica.connector_webhook_details] -merchant_secret="Source verification key" - -[bankofamerica.metadata.apple_pay.session_token_data] -certificate="Merchant Certificate (Base64 Encoded)" -certificate_keys="Merchant PrivateKey (Base64 Encoded)" -merchant_identifier="Apple Merchant Identifier" -display_name="Display Name" -initiative="Domain" -initiative_context="Domain Name" -[bankofamerica.metadata.apple_pay.payment_request_data] -supported_networks=["visa","masterCard","amex","discover"] -merchant_capabilities=["supports3DS"] -label="apple" - -[bankofamerica.metadata.google_pay] -merchant_name="Google Pay Merchant Name" -gateway_merchant_id="Google Pay Merchant Key" -merchant_id="Google Pay Merchant ID" - -[placetopay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] - -[placetopay.connector_auth.BodyKey] -api_key="Login" -key1="Trankey" \ No newline at end of file +[wise_payout] +[[wise_payout.bank_transfer]] + payment_method_type = "ach" +[[wise_payout.bank_transfer]] + payment_method_type = "bacs" +[[wise_payout.bank_transfer]] + payment_method_type = "sepa" +[wise_payout.connector_auth.BodyKey] +api_key = "Wise API Key" +key1 = "Wise Account Id" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 225fc63912fc..38a41b40f7a7 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1,9 +1,56 @@ - [aci] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["ali_pay","mb_way"] -bank_redirect=["ideal","giropay","sofort","eps","przelewy24","trustly"] +[[aci.credit]] + payment_method_type = "Mastercard" +[[aci.credit]] + payment_method_type = "Visa" +[[aci.credit]] + payment_method_type = "Interac" +[[aci.credit]] + payment_method_type = "AmericanExpress" +[[aci.credit]] + payment_method_type = "JCB" +[[aci.credit]] + payment_method_type = "DinersClub" +[[aci.credit]] + payment_method_type = "Discover" +[[aci.credit]] + payment_method_type = "CartesBancaires" +[[aci.credit]] + payment_method_type = "UnionPay" +[[aci.debit]] + payment_method_type = "Mastercard" +[[aci.debit]] + payment_method_type = "Visa" +[[aci.debit]] + payment_method_type = "Interac" +[[aci.debit]] + payment_method_type = "AmericanExpress" +[[aci.debit]] + payment_method_type = "JCB" +[[aci.debit]] + payment_method_type = "DinersClub" +[[aci.debit]] + payment_method_type = "Discover" +[[aci.debit]] + payment_method_type = "CartesBancaires" +[[aci.debit]] + payment_method_type = "UnionPay" +[[aci.wallet]] + payment_method_type = "ali_pay" +[[aci.wallet]] + payment_method_type = "mb_way" +[[aci.bank_redirect]] + payment_method_type = "ideal" +[[aci.bank_redirect]] + payment_method_type = "giropay" +[[aci.bank_redirect]] + payment_method_type = "sofort" +[[aci.bank_redirect]] + payment_method_type = "eps" +[[aci.bank_redirect]] + payment_method_type = "przelewy24" +[[aci.bank_redirect]] + payment_method_type = "trustly" [aci.connector_auth.BodyKey] api_key="API Key" key1="Entity ID" @@ -11,12 +58,66 @@ key1="Entity ID" merchant_secret="Source verification key" [adyen] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","affirm","afterpay_clearpay"] -bank_debit=["ach","bacs"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["apple_pay","google_pay","paypal"] +[[adyen.credit]] + payment_method_type = "Mastercard" +[[adyen.credit]] + payment_method_type = "Visa" +[[adyen.credit]] + payment_method_type = "Interac" +[[adyen.credit]] + payment_method_type = "AmericanExpress" +[[adyen.credit]] + payment_method_type = "JCB" +[[adyen.credit]] + payment_method_type = "DinersClub" +[[adyen.credit]] + payment_method_type = "Discover" +[[adyen.credit]] + payment_method_type = "CartesBancaires" +[[adyen.credit]] + payment_method_type = "UnionPay" +[[adyen.debit]] + payment_method_type = "Mastercard" +[[adyen.debit]] + payment_method_type = "Visa" +[[adyen.debit]] + payment_method_type = "Interac" +[[adyen.debit]] + payment_method_type = "AmericanExpress" +[[adyen.debit]] + payment_method_type = "JCB" +[[adyen.debit]] + payment_method_type = "DinersClub" +[[adyen.debit]] + payment_method_type = "Discover" +[[adyen.debit]] + payment_method_type = "CartesBancaires" +[[adyen.debit]] + payment_method_type = "UnionPay" +[[adyen.pay_later]] + payment_method_type = "klarna" +[[adyen.pay_later]] + payment_method_type = "affirm" +[[adyen.pay_later]] + payment_method_type = "afterpay_clearpay" +[[adyen.bank_debit]] + payment_method_type = "ach" +[[adyen.bank_debit]] + payment_method_type = "bacs" +[[adyen.bank_redirect]] + payment_method_type = "ideal" +[[adyen.bank_redirect]] + payment_method_type = "giropay" +[[adyen.bank_redirect]] + payment_method_type = "sofort" +[[adyen.bank_redirect]] + payment_method_type = "eps" +[[adyen.wallet]] + payment_method_type = "apple_pay" +[[adyen.wallet]] + payment_method_type = "google_pay" +[[adyen.wallet]] + payment_method_type = "paypal" [adyen.connector_auth.BodyKey] api_key="Adyen API Key" key1="Adyen Account Id" @@ -43,8 +144,42 @@ label="apple" [airwallex] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[airwallex.credit]] + payment_method_type = "Mastercard" +[[airwallex.credit]] + payment_method_type = "Visa" +[[airwallex.credit]] + payment_method_type = "Interac" +[[airwallex.credit]] + payment_method_type = "AmericanExpress" +[[airwallex.credit]] + payment_method_type = "JCB" +[[airwallex.credit]] + payment_method_type = "DinersClub" +[[airwallex.credit]] + payment_method_type = "Discover" +[[airwallex.credit]] + payment_method_type = "CartesBancaires" +[[airwallex.credit]] + payment_method_type = "UnionPay" +[[airwallex.debit]] + payment_method_type = "Mastercard" +[[airwallex.debit]] + payment_method_type = "Visa" +[[airwallex.debit]] + payment_method_type = "Interac" +[[airwallex.debit]] + payment_method_type = "AmericanExpress" +[[airwallex.debit]] + payment_method_type = "JCB" +[[airwallex.debit]] + payment_method_type = "DinersClub" +[[airwallex.debit]] + payment_method_type = "Discover" +[[airwallex.debit]] + payment_method_type = "CartesBancaires" +[[airwallex.debit]] + payment_method_type = "UnionPay" body_type="BodyKey" [airwallex.connector_auth.BodyKey] api_key="API Key" @@ -53,9 +188,46 @@ key1="Client ID" merchant_secret="Source verification key" [authorizedotnet] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","paypal"] +[[authorizedotnet.credit]] + payment_method_type = "Mastercard" +[[authorizedotnet.credit]] + payment_method_type = "Visa" +[[authorizedotnet.credit]] + payment_method_type = "Interac" +[[authorizedotnet.credit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.credit]] + payment_method_type = "JCB" +[[authorizedotnet.credit]] + payment_method_type = "DinersClub" +[[authorizedotnet.credit]] + payment_method_type = "Discover" +[[authorizedotnet.credit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.credit]] + payment_method_type = "UnionPay" +[[authorizedotnet.debit]] + payment_method_type = "Mastercard" +[[authorizedotnet.debit]] + payment_method_type = "Visa" +[[authorizedotnet.debit]] + payment_method_type = "Interac" +[[authorizedotnet.debit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.debit]] + payment_method_type = "JCB" +[[authorizedotnet.debit]] + payment_method_type = "DinersClub" +[[authorizedotnet.debit]] + payment_method_type = "Discover" +[[authorizedotnet.debit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.debit]] + payment_method_type = "UnionPay" +[[authorizedotnet.wallet]] + payment_method_type = "google_pay" +[[authorizedotnet.wallet]] + payment_method_type = "paypal" body_type="BodyKey" [authorizedotnet.connector_auth.BodyKey] api_key="API Login ID" @@ -68,16 +240,54 @@ merchant_id="Google Pay Merchant ID" merchant_secret="Source verification key" [bitpay] -crypto = ["crypto_currency"] +[[bitpay.crypto]] + payment_method_type = "crypto_currency" [bitpay.connector_auth.HeaderKey] api_key="API Key" [bitpay.connector_webhook_details] merchant_secret="Source verification key" [bluesnap] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","apple_pay"] +[[bluesnap.credit]] + payment_method_type = "Mastercard" +[[bluesnap.credit]] + payment_method_type = "Visa" +[[bluesnap.credit]] + payment_method_type = "Interac" +[[bluesnap.credit]] + payment_method_type = "AmericanExpress" +[[bluesnap.credit]] + payment_method_type = "JCB" +[[bluesnap.credit]] + payment_method_type = "DinersClub" +[[bluesnap.credit]] + payment_method_type = "Discover" +[[bluesnap.credit]] + payment_method_type = "CartesBancaires" +[[bluesnap.credit]] + payment_method_type = "UnionPay" +[[bluesnap.debit]] + payment_method_type = "Mastercard" +[[bluesnap.debit]] + payment_method_type = "Visa" +[[bluesnap.debit]] + payment_method_type = "Interac" +[[bluesnap.debit]] + payment_method_type = "AmericanExpress" +[[bluesnap.debit]] + payment_method_type = "JCB" +[[bluesnap.debit]] + payment_method_type = "DinersClub" +[[bluesnap.debit]] + payment_method_type = "Discover" +[[bluesnap.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[bluesnap.wallet]] + payment_method_type = "google_pay" +[[bluesnap.wallet]] + payment_method_type = "apple_pay" [bluesnap.connector_auth.BodyKey] api_key="Password" key1="Username" @@ -104,8 +314,44 @@ label="apple" merchant_id="Merchant Id" [braintree] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[braintree.credit]] + payment_method_type = "Mastercard" +[[braintree.credit]] + payment_method_type = "Visa" +[[braintree.credit]] + payment_method_type = "Interac" +[[braintree.credit]] + payment_method_type = "AmericanExpress" +[[braintree.credit]] + payment_method_type = "JCB" +[[braintree.credit]] + payment_method_type = "DinersClub" +[[braintree.credit]] + payment_method_type = "Discover" +[[braintree.credit]] + payment_method_type = "CartesBancaires" +[[braintree.credit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "Mastercard" +[[braintree.debit]] + payment_method_type = "Visa" +[[braintree.debit]] + payment_method_type = "Interac" +[[braintree.debit]] + payment_method_type = "AmericanExpress" +[[braintree.debit]] + payment_method_type = "JCB" +[[braintree.debit]] + payment_method_type = "DinersClub" +[[braintree.debit]] + payment_method_type = "Discover" +[[braintree.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "UnionPay" [braintree.connector_auth.SignatureKey] api_key="Public Key" @@ -114,10 +360,212 @@ api_secret="Private Key" [braintree.connector_webhook_details] merchant_secret="Source verification key" +[bambora] +[[bambora.credit]] + payment_method_type = "Mastercard" +[[bambora.credit]] + payment_method_type = "Visa" +[[bambora.credit]] + payment_method_type = "Interac" +[[bambora.credit]] + payment_method_type = "AmericanExpress" +[[bambora.credit]] + payment_method_type = "JCB" +[[bambora.credit]] + payment_method_type = "DinersClub" +[[bambora.credit]] + payment_method_type = "Discover" +[[bambora.credit]] + payment_method_type = "CartesBancaires" +[[bambora.credit]] + payment_method_type = "UnionPay" +[[bambora.debit]] + payment_method_type = "Mastercard" +[[bambora.debit]] + payment_method_type = "Visa" +[[bambora.debit]] + payment_method_type = "Interac" +[[bambora.debit]] + payment_method_type = "AmericanExpress" +[[bambora.debit]] + payment_method_type = "JCB" +[[bambora.debit]] + payment_method_type = "DinersClub" +[[bambora.debit]] + payment_method_type = "Discover" +[[bambora.debit]] + payment_method_type = "CartesBancaires" +[[bambora.debit]] + payment_method_type = "UnionPay" +[[bambora.wallet]] + payment_method_type = "apple_pay" +[[bambora.wallet]] + payment_method_type = "paypal" +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" + +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica] +[[bankofamerica.credit]] + payment_method_type = "Mastercard" +[[bankofamerica.credit]] + payment_method_type = "Visa" +[[bankofamerica.credit]] + payment_method_type = "Interac" +[[bankofamerica.credit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.credit]] + payment_method_type = "JCB" +[[bankofamerica.credit]] + payment_method_type = "DinersClub" +[[bankofamerica.credit]] + payment_method_type = "Discover" +[[bankofamerica.credit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.credit]] + payment_method_type = "UnionPay" +[[bankofamerica.debit]] + payment_method_type = "Mastercard" +[[bankofamerica.debit]] + payment_method_type = "Visa" +[[bankofamerica.debit]] + payment_method_type = "Interac" +[[bankofamerica.debit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.debit]] + payment_method_type = "JCB" +[[bankofamerica.debit]] + payment_method_type = "DinersClub" +[[bankofamerica.debit]] + payment_method_type = "Discover" +[[bankofamerica.debit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.debit]] + payment_method_type = "UnionPay" +[[bankofamerica.wallet]] + payment_method_type = "apple_pay" +[[bankofamerica.wallet]] + payment_method_type = "google_pay" + +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + +[cashtocode] +[[cashtocode.reward]] + payment_method_type = "classic" +[[cashtocode.reward]] + payment_method_type = "evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" + + +[cryptopay] +[[cryptopay.crypto]] + payment_method_type = "crypto_currency" +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + [checkout] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] +[[checkout.credit]] + payment_method_type = "Mastercard" +[[checkout.credit]] + payment_method_type = "Visa" +[[checkout.credit]] + payment_method_type = "Interac" +[[checkout.credit]] + payment_method_type = "AmericanExpress" +[[checkout.credit]] + payment_method_type = "JCB" +[[checkout.credit]] + payment_method_type = "DinersClub" +[[checkout.credit]] + payment_method_type = "Discover" +[[checkout.credit]] + payment_method_type = "CartesBancaires" +[[checkout.credit]] + payment_method_type = "UnionPay" +[[checkout.debit]] + payment_method_type = "Mastercard" +[[checkout.debit]] + payment_method_type = "Visa" +[[checkout.debit]] + payment_method_type = "Interac" +[[checkout.debit]] + payment_method_type = "AmericanExpress" +[[checkout.debit]] + payment_method_type = "JCB" +[[checkout.debit]] + payment_method_type = "DinersClub" +[[checkout.debit]] + payment_method_type = "Discover" +[[checkout.debit]] + payment_method_type = "CartesBancaires" +[[checkout.debit]] + payment_method_type = "UnionPay" +[[checkout.wallet]] + payment_method_type = "apple_pay" +[[checkout.wallet]] + payment_method_type = "google_pay" [checkout.connector_auth.SignatureKey] api_key="Checkout API Public Key" key1="Processing Channel ID" @@ -145,16 +593,54 @@ label="apple" [coinbase] -crypto = ["crypto_currency"] +[[coinbase.crypto]] + payment_method_type = "crypto_currency" [coinbase.connector_auth.HeaderKey] api_key="API Key" [coinbase.connector_webhook_details] merchant_secret="Source verification key" [cybersource] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] +[[cybersource.credit]] + payment_method_type = "Mastercard" +[[cybersource.credit]] + payment_method_type = "Visa" +[[cybersource.credit]] + payment_method_type = "Interac" +[[cybersource.credit]] + payment_method_type = "AmericanExpress" +[[cybersource.credit]] + payment_method_type = "JCB" +[[cybersource.credit]] + payment_method_type = "DinersClub" +[[cybersource.credit]] + payment_method_type = "Discover" +[[cybersource.credit]] + payment_method_type = "CartesBancaires" +[[cybersource.credit]] + payment_method_type = "UnionPay" +[[cybersource.debit]] + payment_method_type = "Mastercard" +[[cybersource.debit]] + payment_method_type = "Visa" +[[cybersource.debit]] + payment_method_type = "Interac" +[[cybersource.debit]] + payment_method_type = "AmericanExpress" +[[cybersource.debit]] + payment_method_type = "JCB" +[[cybersource.debit]] + payment_method_type = "DinersClub" +[[cybersource.debit]] + payment_method_type = "Discover" +[[cybersource.debit]] + payment_method_type = "CartesBancaires" +[[cybersource.debit]] + payment_method_type = "UnionPay" +[[cybersource.wallet]] + payment_method_type = "apple_pay" +[[cybersource.wallet]] + payment_method_type = "google_pay" [cybersource.connector_auth.SignatureKey] api_key="Key" key1="Merchant ID" @@ -179,47 +665,43 @@ merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" -[iatapay] -upi=["upi_collect"] -[iatapay.connector_auth.SignatureKey] -api_key="Client ID" -key1="Airline ID" -api_secret="Client Secret" -[iatapay.connector_webhook_details] -merchant_secret="Source verification key" - -[opennode] -crypto = ["crypto_currency"] -[opennode.connector_auth.HeaderKey] -api_key="API Key" -[opennode.connector_webhook_details] -merchant_secret="Source verification key" - -[bambora] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","paypal"] -[bambora.connector_auth.BodyKey] -api_key="Passcode" -key1="Merchant Id" -[bambora.connector_webhook_details] -merchant_secret="Source verification key" - -[bambora.metadata.apple_pay.session_token_data] -certificate="Merchant Certificate (Base64 Encoded)" -certificate_keys="Merchant PrivateKey (Base64 Encoded)" -merchant_identifier="Apple Merchant Identifier" -display_name="Display Name" -initiative="Domain" -initiative_context="Domain Name" -[bambora.metadata.apple_pay.payment_request_data] -supported_networks=["visa","masterCard","amex","discover"] -merchant_capabilities=["supports3DS"] -label="apple" - [dlocal] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[dlocal.credit]] + payment_method_type = "Mastercard" +[[dlocal.credit]] + payment_method_type = "Visa" +[[dlocal.credit]] + payment_method_type = "Interac" +[[dlocal.credit]] + payment_method_type = "AmericanExpress" +[[dlocal.credit]] + payment_method_type = "JCB" +[[dlocal.credit]] + payment_method_type = "DinersClub" +[[dlocal.credit]] + payment_method_type = "Discover" +[[dlocal.credit]] + payment_method_type = "CartesBancaires" +[[dlocal.credit]] + payment_method_type = "UnionPay" +[[dlocal.debit]] + payment_method_type = "Mastercard" +[[dlocal.debit]] + payment_method_type = "Visa" +[[dlocal.debit]] + payment_method_type = "Interac" +[[dlocal.debit]] + payment_method_type = "AmericanExpress" +[[dlocal.debit]] + payment_method_type = "JCB" +[[dlocal.debit]] + payment_method_type = "DinersClub" +[[dlocal.debit]] + payment_method_type = "Discover" +[[dlocal.debit]] + payment_method_type = "CartesBancaires" +[[dlocal.debit]] + payment_method_type = "UnionPay" [dlocal.connector_auth.SignatureKey] api_key="X Login" key1="X Trans Key" @@ -229,8 +711,42 @@ merchant_secret="Source verification key" [fiserv] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[fiserv.credit]] + payment_method_type = "Mastercard" +[[fiserv.credit]] + payment_method_type = "Visa" +[[fiserv.credit]] + payment_method_type = "Interac" +[[fiserv.credit]] + payment_method_type = "AmericanExpress" +[[fiserv.credit]] + payment_method_type = "JCB" +[[fiserv.credit]] + payment_method_type = "DinersClub" +[[fiserv.credit]] + payment_method_type = "Discover" +[[fiserv.credit]] + payment_method_type = "CartesBancaires" +[[fiserv.credit]] + payment_method_type = "UnionPay" +[[fiserv.debit]] + payment_method_type = "Mastercard" +[[fiserv.debit]] + payment_method_type = "Visa" +[[fiserv.debit]] + payment_method_type = "Interac" +[[fiserv.debit]] + payment_method_type = "AmericanExpress" +[[fiserv.debit]] + payment_method_type = "JCB" +[[fiserv.debit]] + payment_method_type = "DinersClub" +[[fiserv.debit]] + payment_method_type = "Discover" +[[fiserv.debit]] + payment_method_type = "CartesBancaires" +[[fiserv.debit]] + payment_method_type = "UnionPay" [fiserv.connector_auth.SignatureKey] api_key="API Key" key1="Merchant ID" @@ -241,8 +757,42 @@ merchant_secret="Source verification key" terminal_id="Terminal ID" [forte] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[forte.credit]] + payment_method_type = "Mastercard" +[[forte.credit]] + payment_method_type = "Visa" +[[forte.credit]] + payment_method_type = "Interac" +[[forte.credit]] + payment_method_type = "AmericanExpress" +[[forte.credit]] + payment_method_type = "JCB" +[[forte.credit]] + payment_method_type = "DinersClub" +[[forte.credit]] + payment_method_type = "Discover" +[[forte.credit]] + payment_method_type = "CartesBancaires" +[[forte.credit]] + payment_method_type = "UnionPay" +[[forte.debit]] + payment_method_type = "Mastercard" +[[forte.debit]] + payment_method_type = "Visa" +[[forte.debit]] + payment_method_type = "Interac" +[[forte.debit]] + payment_method_type = "AmericanExpress" +[[forte.debit]] + payment_method_type = "JCB" +[[forte.debit]] + payment_method_type = "DinersClub" +[[forte.debit]] + payment_method_type = "Discover" +[[forte.debit]] + payment_method_type = "CartesBancaires" +[[forte.debit]] + payment_method_type = "UnionPay" [forte.connector_auth.MultiAuthKey] api_key="API Access ID" key1="Organization ID" @@ -253,36 +803,144 @@ merchant_secret="Source verification key" [globalpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["google_pay","paypal"] +[[globalpay.credit]] + payment_method_type = "Mastercard" +[[globalpay.credit]] + payment_method_type = "Visa" +[[globalpay.credit]] + payment_method_type = "Interac" +[[globalpay.credit]] + payment_method_type = "AmericanExpress" +[[globalpay.credit]] + payment_method_type = "JCB" +[[globalpay.credit]] + payment_method_type = "DinersClub" +[[globalpay.credit]] + payment_method_type = "Discover" +[[globalpay.credit]] + payment_method_type = "CartesBancaires" +[[globalpay.credit]] + payment_method_type = "UnionPay" +[[globalpay.debit]] + payment_method_type = "Mastercard" +[[globalpay.debit]] + payment_method_type = "Visa" +[[globalpay.debit]] + payment_method_type = "Interac" +[[globalpay.debit]] + payment_method_type = "AmericanExpress" +[[globalpay.debit]] + payment_method_type = "JCB" +[[globalpay.debit]] + payment_method_type = "DinersClub" +[[globalpay.debit]] + payment_method_type = "Discover" +[[globalpay.debit]] + payment_method_type = "CartesBancaires" +[[globalpay.debit]] + payment_method_type = "UnionPay" +[[globalpay.bank_redirect]] + payment_method_type = "ideal" +[[globalpay.bank_redirect]] + payment_method_type = "giropay" +[[globalpay.bank_redirect]] + payment_method_type = "sofort" +[[globalpay.bank_redirect]] + payment_method_type = "eps" +[[globalpay.wallet]] + payment_method_type = "google_pay" +[[globalpay.wallet]] + payment_method_type = "paypal" [globalpay.connector_auth.BodyKey] api_key="Global App Key" key1="Global App ID" [globalpay.connector_webhook_details] merchant_secret="Source verification key" - [globalpay.metadata] account_name="Account Name" - [globalpay.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" + +[globepay] +[[globepay.wallet]] + payment_method_type = "we_chat_pay" +[[globepay.wallet]] + payment_method_type = "ali_pay" +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +[[iatapay.upi]] + payment_method_type = "upi_collect" +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + [klarna] -pay_later=["klarna"] +[[klarna.pay_later]] + payment_method_type = "klarna" [klarna.connector_auth.HeaderKey] api_key="Klarna API Key" [klarna.connector_webhook_details] merchant_secret="Source verification key" + [mollie] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["paypal"] +[[mollie.credit]] + payment_method_type = "Mastercard" +[[mollie.credit]] + payment_method_type = "Visa" +[[mollie.credit]] + payment_method_type = "Interac" +[[mollie.credit]] + payment_method_type = "AmericanExpress" +[[mollie.credit]] + payment_method_type = "JCB" +[[mollie.credit]] + payment_method_type = "DinersClub" +[[mollie.credit]] + payment_method_type = "Discover" +[[mollie.credit]] + payment_method_type = "CartesBancaires" +[[mollie.credit]] + payment_method_type = "UnionPay" +[[mollie.debit]] + payment_method_type = "Mastercard" +[[mollie.debit]] + payment_method_type = "Visa" +[[mollie.debit]] + payment_method_type = "Interac" +[[mollie.debit]] + payment_method_type = "AmericanExpress" +[[mollie.debit]] + payment_method_type = "JCB" +[[mollie.debit]] + payment_method_type = "DinersClub" +[[mollie.debit]] + payment_method_type = "Discover" +[[mollie.debit]] + payment_method_type = "CartesBancaires" +[[mollie.debit]] + payment_method_type = "UnionPay" +[[mollie.bank_redirect]] + payment_method_type = "ideal" +[[mollie.bank_redirect]] + payment_method_type = "giropay" +[[mollie.bank_redirect]] + payment_method_type = "sofort" +[[mollie.bank_redirect]] + payment_method_type = "eps" +[[mollie.wallet]] + payment_method_type = "paypal" [mollie.connector_auth.BodyKey] api_key="API Key" key1="Profile Token" @@ -290,8 +948,42 @@ key1="Profile Token" merchant_secret="Source verification key" [multisafepay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[multisafepay.credit]] + payment_method_type = "Mastercard" +[[multisafepay.credit]] + payment_method_type = "Visa" +[[multisafepay.credit]] + payment_method_type = "Interac" +[[multisafepay.credit]] + payment_method_type = "AmericanExpress" +[[multisafepay.credit]] + payment_method_type = "JCB" +[[multisafepay.credit]] + payment_method_type = "DinersClub" +[[multisafepay.credit]] + payment_method_type = "Discover" +[[multisafepay.credit]] + payment_method_type = "CartesBancaires" +[[multisafepay.credit]] + payment_method_type = "UnionPay" +[[multisafepay.debit]] + payment_method_type = "Mastercard" +[[multisafepay.debit]] + payment_method_type = "Visa" +[[multisafepay.debit]] + payment_method_type = "Interac" +[[multisafepay.debit]] + payment_method_type = "AmericanExpress" +[[multisafepay.debit]] + payment_method_type = "JCB" +[[multisafepay.debit]] + payment_method_type = "DinersClub" +[[multisafepay.debit]] + payment_method_type = "Discover" +[[multisafepay.debit]] + payment_method_type = "CartesBancaires" +[[multisafepay.debit]] + payment_method_type = "UnionPay" [multisafepay.connector_auth.HeaderKey] api_key="Enter API Key" [multisafepay.connector_webhook_details] @@ -299,16 +991,59 @@ merchant_secret="Source verification key" [nexinets] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["apple_pay","paypal"] +[[nexinets.credit]] + payment_method_type = "Mastercard" +[[nexinets.credit]] + payment_method_type = "Visa" +[[nexinets.credit]] + payment_method_type = "Interac" +[[nexinets.credit]] + payment_method_type = "AmericanExpress" +[[nexinets.credit]] + payment_method_type = "JCB" +[[nexinets.credit]] + payment_method_type = "DinersClub" +[[nexinets.credit]] + payment_method_type = "Discover" +[[nexinets.credit]] + payment_method_type = "CartesBancaires" +[[nexinets.credit]] + payment_method_type = "UnionPay" +[[nexinets.debit]] + payment_method_type = "Mastercard" +[[nexinets.debit]] + payment_method_type = "Visa" +[[nexinets.debit]] + payment_method_type = "Interac" +[[nexinets.debit]] + payment_method_type = "AmericanExpress" +[[nexinets.debit]] + payment_method_type = "JCB" +[[nexinets.debit]] + payment_method_type = "DinersClub" +[[nexinets.debit]] + payment_method_type = "Discover" +[[nexinets.debit]] + payment_method_type = "CartesBancaires" +[[nexinets.debit]] + payment_method_type = "UnionPay" +[[nexinets.bank_redirect]] + payment_method_type = "ideal" +[[nexinets.bank_redirect]] + payment_method_type = "giropay" +[[nexinets.bank_redirect]] + payment_method_type = "sofort" +[[nexinets.bank_redirect]] + payment_method_type = "eps" +[[nexinets.wallet]] + payment_method_type = "apple_pay" +[[nexinets.wallet]] + payment_method_type = "paypal" [nexinets.connector_auth.BodyKey] api_key="API Key" key1="Merchant ID" [nexinets.connector_webhook_details] merchant_secret="Source verification key" - [nexinets.metadata.apple_pay.session_token_data] certificate="Merchant Certificate (Base64 Encoded)" certificate_keys="Merchant PrivateKey (Base64 Encoded)" @@ -322,7 +1057,8 @@ merchant_capabilities=["supports3DS"] label="apple" [nmi] -bank_redirect=["ideal"] +[[nmi.bank_redirect]] + payment_method_type = "ideal" [nmi.connector_auth.SignatureKey] api_key="Client ID" key1="Airline ID" @@ -331,8 +1067,42 @@ api_secret="Client Secret" merchant_secret="Source verification key" [nuvei] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[nuvei.credit]] + payment_method_type = "Mastercard" +[[nuvei.credit]] + payment_method_type = "Visa" +[[nuvei.credit]] + payment_method_type = "Interac" +[[nuvei.credit]] + payment_method_type = "AmericanExpress" +[[nuvei.credit]] + payment_method_type = "JCB" +[[nuvei.credit]] + payment_method_type = "DinersClub" +[[nuvei.credit]] + payment_method_type = "Discover" +[[nuvei.credit]] + payment_method_type = "CartesBancaires" +[[nuvei.credit]] + payment_method_type = "UnionPay" +[[nuvei.debit]] + payment_method_type = "Mastercard" +[[nuvei.debit]] + payment_method_type = "Visa" +[[nuvei.debit]] + payment_method_type = "Interac" +[[nuvei.debit]] + payment_method_type = "AmericanExpress" +[[nuvei.debit]] + payment_method_type = "JCB" +[[nuvei.debit]] + payment_method_type = "DinersClub" +[[nuvei.debit]] + payment_method_type = "Discover" +[[nuvei.debit]] + payment_method_type = "CartesBancaires" +[[nuvei.debit]] + payment_method_type = "UnionPay" [nuvei.connector_auth.SignatureKey] api_key="Merchant ID" key1="Merchant Site ID" @@ -340,10 +1110,56 @@ api_secret="Merchant Secret" [nuvei.connector_webhook_details] merchant_secret="Source verification key" + + + +[opennode] +[[opennode.crypto]] + payment_method_type = "crypto_currency" +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + [paypal] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["paypal"] +[[paypal.credit]] + payment_method_type = "Mastercard" +[[paypal.credit]] + payment_method_type = "Visa" +[[paypal.credit]] + payment_method_type = "Interac" +[[paypal.credit]] + payment_method_type = "AmericanExpress" +[[paypal.credit]] + payment_method_type = "JCB" +[[paypal.credit]] + payment_method_type = "DinersClub" +[[paypal.credit]] + payment_method_type = "Discover" +[[paypal.credit]] + payment_method_type = "CartesBancaires" +[[paypal.credit]] + payment_method_type = "UnionPay" +[[paypal.debit]] + payment_method_type = "Mastercard" +[[paypal.debit]] + payment_method_type = "Visa" +[[paypal.debit]] + payment_method_type = "Interac" +[[paypal.debit]] + payment_method_type = "AmericanExpress" +[[paypal.debit]] + payment_method_type = "JCB" +[[paypal.debit]] + payment_method_type = "DinersClub" +[[paypal.debit]] + payment_method_type = "Discover" +[[paypal.debit]] + payment_method_type = "CartesBancaires" +[[paypal.debit]] + payment_method_type = "UnionPay" +[[paypal.wallet]] + payment_method_type = "paypal" is_verifiable = true [paypal.connector_auth.BodyKey] api_key="Client Secret" @@ -352,9 +1168,44 @@ key1="Client ID" merchant_secret="Source verification key" [payu] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay"] +[[payu.credit]] + payment_method_type = "Mastercard" +[[payu.credit]] + payment_method_type = "Visa" +[[payu.credit]] + payment_method_type = "Interac" +[[payu.credit]] + payment_method_type = "AmericanExpress" +[[payu.credit]] + payment_method_type = "JCB" +[[payu.credit]] + payment_method_type = "DinersClub" +[[payu.credit]] + payment_method_type = "Discover" +[[payu.credit]] + payment_method_type = "CartesBancaires" +[[payu.credit]] + payment_method_type = "UnionPay" +[[payu.debit]] + payment_method_type = "Mastercard" +[[payu.debit]] + payment_method_type = "Visa" +[[payu.debit]] + payment_method_type = "Interac" +[[payu.debit]] + payment_method_type = "AmericanExpress" +[[payu.debit]] + payment_method_type = "JCB" +[[payu.debit]] + payment_method_type = "DinersClub" +[[payu.debit]] + payment_method_type = "Discover" +[[payu.debit]] + payment_method_type = "CartesBancaires" +[[payu.debit]] + payment_method_type = "UnionPay" +[[payu.wallet]] + payment_method_type = "google_pay" [payu.connector_auth.BodyKey] api_key="API Key" key1="Merchant POS ID" @@ -367,9 +1218,44 @@ gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" [rapyd] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay"] +[[rapyd.credit]] + payment_method_type = "Mastercard" +[[rapyd.credit]] + payment_method_type = "Visa" +[[rapyd.credit]] + payment_method_type = "Interac" +[[rapyd.credit]] + payment_method_type = "AmericanExpress" +[[rapyd.credit]] + payment_method_type = "JCB" +[[rapyd.credit]] + payment_method_type = "DinersClub" +[[rapyd.credit]] + payment_method_type = "Discover" +[[rapyd.credit]] + payment_method_type = "CartesBancaires" +[[rapyd.credit]] + payment_method_type = "UnionPay" +[[rapyd.debit]] + payment_method_type = "Mastercard" +[[rapyd.debit]] + payment_method_type = "Visa" +[[rapyd.debit]] + payment_method_type = "Interac" +[[rapyd.debit]] + payment_method_type = "AmericanExpress" +[[rapyd.debit]] + payment_method_type = "JCB" +[[rapyd.debit]] + payment_method_type = "DinersClub" +[[rapyd.debit]] + payment_method_type = "Discover" +[[rapyd.debit]] + payment_method_type = "CartesBancaires" +[[rapyd.debit]] + payment_method_type = "UnionPay" +[[rapyd.wallet]] + payment_method_type = "apple_pay" [rapyd.connector_auth.BodyKey] api_key="Access Key" key1="API Secret" @@ -389,33 +1275,131 @@ merchant_capabilities=["supports3DS"] label="apple" [shift4] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] +[[shift4.credit]] + payment_method_type = "Mastercard" +[[shift4.credit]] + payment_method_type = "Visa" +[[shift4.credit]] + payment_method_type = "Interac" +[[shift4.credit]] + payment_method_type = "AmericanExpress" +[[shift4.credit]] + payment_method_type = "JCB" +[[shift4.credit]] + payment_method_type = "DinersClub" +[[shift4.credit]] + payment_method_type = "Discover" +[[shift4.credit]] + payment_method_type = "CartesBancaires" +[[shift4.credit]] + payment_method_type = "UnionPay" +[[shift4.debit]] + payment_method_type = "Mastercard" +[[shift4.debit]] + payment_method_type = "Visa" +[[shift4.debit]] + payment_method_type = "Interac" +[[shift4.debit]] + payment_method_type = "AmericanExpress" +[[shift4.debit]] + payment_method_type = "JCB" +[[shift4.debit]] + payment_method_type = "DinersClub" +[[shift4.debit]] + payment_method_type = "Discover" +[[shift4.debit]] + payment_method_type = "CartesBancaires" +[[shift4.debit]] + payment_method_type = "UnionPay" +[[shift4.bank_redirect]] + payment_method_type = "ideal" +[[shift4.bank_redirect]] + payment_method_type = "giropay" +[[shift4.bank_redirect]] + payment_method_type = "sofort" +[[shift4.bank_redirect]] + payment_method_type = "eps" [shift4.connector_auth.HeaderKey] api_key="API Key" [shift4.connector_webhook_details] merchant_secret="Source verification key" [stripe] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","affirm","afterpay_clearpay"] -bank_redirect=["ideal","giropay","sofort","eps"] -bank_debit=["ach","becs","sepa"] -bank_transfer=["ach","bacs","sepa"] -wallet = ["apple_pay","google_pay"] +[[stripe.credit]] + payment_method_type = "Mastercard" +[[stripe.credit]] + payment_method_type = "Visa" +[[stripe.credit]] + payment_method_type = "Interac" +[[stripe.credit]] + payment_method_type = "AmericanExpress" +[[stripe.credit]] + payment_method_type = "JCB" +[[stripe.credit]] + payment_method_type = "DinersClub" +[[stripe.credit]] + payment_method_type = "Discover" +[[stripe.credit]] + payment_method_type = "CartesBancaires" +[[stripe.credit]] + payment_method_type = "UnionPay" +[[stripe.debit]] + payment_method_type = "Mastercard" +[[stripe.debit]] + payment_method_type = "Visa" +[[stripe.debit]] + payment_method_type = "Interac" +[[stripe.debit]] + payment_method_type = "AmericanExpress" +[[stripe.debit]] + payment_method_type = "JCB" +[[stripe.debit]] + payment_method_type = "DinersClub" +[[stripe.debit]] + payment_method_type = "Discover" +[[stripe.debit]] + payment_method_type = "CartesBancaires" +[[stripe.debit]] + payment_method_type = "UnionPay" +[[stripe.pay_later]] + payment_method_type = "klarna" +[[stripe.pay_later]] + payment_method_type = "affirm" +[[stripe.pay_later]] + payment_method_type = "afterpay_clearpay" +[[stripe.bank_redirect]] + payment_method_type = "ideal" +[[stripe.bank_redirect]] + payment_method_type = "giropay" +[[stripe.bank_redirect]] + payment_method_type = "sofort" +[[stripe.bank_redirect]] + payment_method_type = "eps" +[[stripe.bank_debit]] + payment_method_type = "ach" +[[stripe.bank_debit]] + payment_method_type = "becs" +[[stripe.bank_debit]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "ach" +[[stripe.bank_transfer]] + payment_method_type = "bacs" +[[stripe.bank_transfer]] + payment_method_type = "sepa" +[[stripe.wallet]] + payment_method_type = "apple_pay" +[[stripe.wallet]] + payment_method_type = "google_pay" is_verifiable = true [stripe.connector_auth.HeaderKey] api_key="Secret Key" [stripe.connector_webhook_details] merchant_secret="Source verification key" - [stripe.metadata.google_pay] merchant_name="Google Pay Merchant Name" stripe_publishable_key="Stripe Publishable Key" merchant_id="Google Pay Merchant ID" - [stripe.metadata.apple_pay.session_token_data] certificate="Merchant Certificate (Base64 Encoded)" certificate_keys="Merchant PrivateKey (Base64 Encoded)" @@ -428,37 +1412,66 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" -[zen] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] -voucher = ["boleto", "efecty", "pago_efectivo", "red_compra", "red_pagos"] -bank_transfer = ["pix", "pse"] -[zen.connector_auth.HeaderKey] -api_key="API Key" -[zen.connector_webhook_details] -merchant_secret="Source verification key" -[zen.metadata.apple_pay] -terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" -[zen.metadata.google_pay] -terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" [trustpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps", "blik"] -wallet = ["apple_pay","google_pay"] +[[trustpay.credit]] + payment_method_type = "Mastercard" +[[trustpay.credit]] + payment_method_type = "Visa" +[[trustpay.credit]] + payment_method_type = "Interac" +[[trustpay.credit]] + payment_method_type = "AmericanExpress" +[[trustpay.credit]] + payment_method_type = "JCB" +[[trustpay.credit]] + payment_method_type = "DinersClub" +[[trustpay.credit]] + payment_method_type = "Discover" +[[trustpay.credit]] + payment_method_type = "CartesBancaires" +[[trustpay.credit]] + payment_method_type = "UnionPay" +[[trustpay.debit]] + payment_method_type = "Mastercard" +[[trustpay.debit]] + payment_method_type = "Visa" +[[trustpay.debit]] + payment_method_type = "Interac" +[[trustpay.debit]] + payment_method_type = "AmericanExpress" +[[trustpay.debit]] + payment_method_type = "JCB" +[[trustpay.debit]] + payment_method_type = "DinersClub" +[[trustpay.debit]] + payment_method_type = "Discover" +[[trustpay.debit]] + payment_method_type = "CartesBancaires" +[[trustpay.debit]] + payment_method_type = "UnionPay" +[[trustpay.bank_redirect]] + payment_method_type = "ideal" +[[trustpay.bank_redirect]] + payment_method_type = "giropay" +[[trustpay.bank_redirect]] + payment_method_type = "sofort" +[[trustpay.bank_redirect]] + payment_method_type = "eps" +[[trustpay.bank_redirect]] + payment_method_type = "blik" +[[trustpay.wallet]] + payment_method_type = "apple_pay" +[[trustpay.wallet]] + payment_method_type = "google_pay" [trustpay.connector_auth.SignatureKey] api_key="API Key" key1="Project ID" api_secret="Secret Key" [trustpay.connector_webhook_details] merchant_secret="Source verification key" - [trustpay.metadata.apple_pay.session_token_data] certificate="Merchant Certificate (Base64 Encoded)" certificate_keys="Merchant PrivateKey (Base64 Encoded)" @@ -472,9 +1485,46 @@ merchant_capabilities=["supports3DS"] label="apple" [worldline] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay"] +[[worldline.credit]] + payment_method_type = "Mastercard" +[[worldline.credit]] + payment_method_type = "Visa" +[[worldline.credit]] + payment_method_type = "Interac" +[[worldline.credit]] + payment_method_type = "AmericanExpress" +[[worldline.credit]] + payment_method_type = "JCB" +[[worldline.credit]] + payment_method_type = "DinersClub" +[[worldline.credit]] + payment_method_type = "Discover" +[[worldline.credit]] + payment_method_type = "CartesBancaires" +[[worldline.credit]] + payment_method_type = "UnionPay" +[[worldline.debit]] + payment_method_type = "Mastercard" +[[worldline.debit]] + payment_method_type = "Visa" +[[worldline.debit]] + payment_method_type = "Interac" +[[worldline.debit]] + payment_method_type = "AmericanExpress" +[[worldline.debit]] + payment_method_type = "JCB" +[[worldline.debit]] + payment_method_type = "DinersClub" +[[worldline.debit]] + payment_method_type = "Discover" +[[worldline.debit]] + payment_method_type = "CartesBancaires" +[[worldline.debit]] + payment_method_type = "UnionPay" +[[worldline.bank_redirect]] + payment_method_type = "ideal" +[[worldline.bank_redirect]] + payment_method_type = "giropay" [worldline.connector_auth.SignatureKey] api_key="API Key ID" key1="Merchant ID" @@ -483,18 +1533,53 @@ api_secret="Secret API Key" merchant_secret="Source verification key" [worldpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","apple_pay"] +[[worldpay.credit]] + payment_method_type = "Mastercard" +[[worldpay.credit]] + payment_method_type = "Visa" +[[worldpay.credit]] + payment_method_type = "Interac" +[[worldpay.credit]] + payment_method_type = "AmericanExpress" +[[worldpay.credit]] + payment_method_type = "JCB" +[[worldpay.credit]] + payment_method_type = "DinersClub" +[[worldpay.credit]] + payment_method_type = "Discover" +[[worldpay.credit]] + payment_method_type = "CartesBancaires" +[[worldpay.credit]] + payment_method_type = "UnionPay" +[[worldpay.debit]] + payment_method_type = "Mastercard" +[[worldpay.debit]] + payment_method_type = "Visa" +[[worldpay.debit]] + payment_method_type = "Interac" +[[worldpay.debit]] + payment_method_type = "AmericanExpress" +[[worldpay.debit]] + payment_method_type = "JCB" +[[worldpay.debit]] + payment_method_type = "DinersClub" +[[worldpay.debit]] + payment_method_type = "Discover" +[[worldpay.debit]] + payment_method_type = "CartesBancaires" +[[worldpay.debit]] + payment_method_type = "UnionPay" +[[worldpay.wallet]] + payment_method_type = "google_pay" +[[worldpay.wallet]] + payment_method_type = "apple_pay" [worldpay.connector_auth.BodyKey] api_key="Username" key1="Password" - [worldpay.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" - [worldpay.metadata.apple_pay.session_token_data] certificate="Merchant Certificate (Base64 Encoded)" certificate_keys="Merchant PrivateKey (Base64 Encoded)" @@ -509,13 +1594,45 @@ label="apple" [worldpay.connector_webhook_details] merchant_secret="Source verification key" -[dummy_connector] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] + [payme] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[payme.credit]] + payment_method_type = "Mastercard" +[[payme.credit]] + payment_method_type = "Visa" +[[payme.credit]] + payment_method_type = "Interac" +[[payme.credit]] + payment_method_type = "AmericanExpress" +[[payme.credit]] + payment_method_type = "JCB" +[[payme.credit]] + payment_method_type = "DinersClub" +[[payme.credit]] + payment_method_type = "Discover" +[[payme.credit]] + payment_method_type = "CartesBancaires" +[[payme.credit]] + payment_method_type = "UnionPay" +[[payme.debit]] + payment_method_type = "Mastercard" +[[payme.debit]] + payment_method_type = "Visa" +[[payme.debit]] + payment_method_type = "Interac" +[[payme.debit]] + payment_method_type = "AmericanExpress" +[[payme.debit]] + payment_method_type = "JCB" +[[payme.debit]] + payment_method_type = "DinersClub" +[[payme.debit]] + payment_method_type = "Discover" +[[payme.debit]] + payment_method_type = "CartesBancaires" +[[payme.debit]] + payment_method_type = "UnionPay" [payme.connector_auth.BodyKey] api_key="Seller Payme Id" key1="Payme Public Key" @@ -524,25 +1641,87 @@ merchant_secret="Payme Client Secret" additional_secret="Payme Client Key" [powertranz] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[powertranz.credit]] + payment_method_type = "Mastercard" +[[powertranz.credit]] + payment_method_type = "Visa" +[[powertranz.credit]] + payment_method_type = "Interac" +[[powertranz.credit]] + payment_method_type = "AmericanExpress" +[[powertranz.credit]] + payment_method_type = "JCB" +[[powertranz.credit]] + payment_method_type = "DinersClub" +[[powertranz.credit]] + payment_method_type = "Discover" +[[powertranz.credit]] + payment_method_type = "CartesBancaires" +[[powertranz.credit]] + payment_method_type = "UnionPay" +[[powertranz.debit]] + payment_method_type = "Mastercard" +[[powertranz.debit]] + payment_method_type = "Visa" +[[powertranz.debit]] + payment_method_type = "Interac" +[[powertranz.debit]] + payment_method_type = "AmericanExpress" +[[powertranz.debit]] + payment_method_type = "JCB" +[[powertranz.debit]] + payment_method_type = "DinersClub" +[[powertranz.debit]] + payment_method_type = "Discover" +[[powertranz.debit]] + payment_method_type = "CartesBancaires" +[[powertranz.debit]] + payment_method_type = "UnionPay" [powertranz.connector_auth.BodyKey] key1 = "PowerTranz Id" api_key="PowerTranz Password" [powertranz.connector_webhook_details] merchant_secret="Source verification key" -[globepay] -wallet = ["we_chat_pay","ali_pay"] -[globepay.connector_auth.BodyKey] -api_key="Partner Code" -key1="Credential Code" -[globepay.connector_webhook_details] -merchant_secret="Source verification key" + [tsys] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[tsys.credit]] + payment_method_type = "Mastercard" +[[tsys.credit]] + payment_method_type = "Visa" +[[tsys.credit]] + payment_method_type = "Interac" +[[tsys.credit]] + payment_method_type = "AmericanExpress" +[[tsys.credit]] + payment_method_type = "JCB" +[[tsys.credit]] + payment_method_type = "DinersClub" +[[tsys.credit]] + payment_method_type = "Discover" +[[tsys.credit]] + payment_method_type = "CartesBancaires" +[[tsys.credit]] + payment_method_type = "UnionPay" +[[tsys.debit]] + payment_method_type = "Mastercard" +[[tsys.debit]] + payment_method_type = "Visa" +[[tsys.debit]] + payment_method_type = "Interac" +[[tsys.debit]] + payment_method_type = "AmericanExpress" +[[tsys.debit]] + payment_method_type = "JCB" +[[tsys.debit]] + payment_method_type = "DinersClub" +[[tsys.debit]] + payment_method_type = "Discover" +[[tsys.debit]] + payment_method_type = "CartesBancaires" +[[tsys.debit]] + payment_method_type = "UnionPay" [tsys.connector_auth.SignatureKey] api_key="Device Id" key1="Transaction Key" @@ -550,65 +1729,68 @@ api_secret="Developer Id" [tsys.connector_webhook_details] merchant_secret="Source verification key" -[cashtocode] -reward = ["classic", "evoucher"] -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_webhook_details] -merchant_secret="Source verification key" - -[cryptopay] -crypto = ["crypto_currency"] -[cryptopay.connector_auth.BodyKey] +[zen] +[[zen.credit]] + payment_method_type = "Mastercard" +[[zen.credit]] + payment_method_type = "Visa" +[[zen.credit]] + payment_method_type = "Interac" +[[zen.credit]] + payment_method_type = "AmericanExpress" +[[zen.credit]] + payment_method_type = "JCB" +[[zen.credit]] + payment_method_type = "DinersClub" +[[zen.credit]] + payment_method_type = "Discover" +[[zen.credit]] + payment_method_type = "CartesBancaires" +[[zen.credit]] + payment_method_type = "UnionPay" +[[zen.debit]] + payment_method_type = "Mastercard" +[[zen.debit]] + payment_method_type = "Visa" +[[zen.debit]] + payment_method_type = "Interac" +[[zen.debit]] + payment_method_type = "AmericanExpress" +[[zen.debit]] + payment_method_type = "JCB" +[[zen.debit]] + payment_method_type = "DinersClub" +[[zen.debit]] + payment_method_type = "Discover" +[[zen.debit]] + payment_method_type = "CartesBancaires" +[[zen.debit]] + payment_method_type = "UnionPay" +[[zen.wallet]] + payment_method_type = "apple_pay" +[[zen.wallet]] + payment_method_type = "google_pay" +[[zen.voucher]] + payment_method_type = "boleto" +[[zen.voucher]] + payment_method_type = "efecty" +[[zen.voucher]] + payment_method_type = "pago_efectivo" +[[zen.voucher]] + payment_method_type = "red_compra" +[[zen.voucher]] + payment_method_type = "red_pagos" +[[zen.bank_transfer]] + payment_method_type = "pix" +[[zen.bank_transfer]] + payment_method_type = "pse" +[zen.connector_auth.HeaderKey] api_key="API Key" -key1="Secret Key" -[cryptopay.connector_webhook_details] -merchant_secret="Source verification key" - -[bankofamerica] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] - -[bankofamerica.connector_auth.SignatureKey] -api_key="Key" -key1="Merchant ID" -api_secret="Shared Secret" -[bankofamerica.connector_webhook_details] +[zen.connector_webhook_details] merchant_secret="Source verification key" - -[bankofamerica.metadata.apple_pay.session_token_data] -certificate="Merchant Certificate (Base64 Encoded)" -certificate_keys="Merchant PrivateKey (Base64 Encoded)" -merchant_identifier="Apple Merchant Identifier" -display_name="Display Name" -initiative="Domain" -initiative_context="Domain Name" -[bankofamerica.metadata.apple_pay.payment_request_data] -supported_networks=["visa","masterCard","amex","discover"] -merchant_capabilities=["supports3DS"] -label="apple" - -[bankofamerica.metadata.google_pay] -merchant_name="Google Pay Merchant Name" -gateway_merchant_id="Google Pay Merchant Key" -merchant_id="Google Pay Merchant ID" \ No newline at end of file +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 44a8806f0fee..c41ad7793e8e 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1,33 +1,235 @@ - [aci] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["ali_pay","mb_way"] -bank_redirect=["ideal","giropay","sofort","eps","przelewy24","trustly","interac"] +[[aci.credit]] + payment_method_type = "Mastercard" +[[aci.credit]] + payment_method_type = "Visa" +[[aci.credit]] + payment_method_type = "Interac" +[[aci.credit]] + payment_method_type = "AmericanExpress" +[[aci.credit]] + payment_method_type = "JCB" +[[aci.credit]] + payment_method_type = "DinersClub" +[[aci.credit]] + payment_method_type = "Discover" +[[aci.credit]] + payment_method_type = "CartesBancaires" +[[aci.credit]] + payment_method_type = "UnionPay" +[[aci.debit]] + payment_method_type = "Mastercard" +[[aci.debit]] + payment_method_type = "Visa" +[[aci.debit]] + payment_method_type = "Interac" +[[aci.debit]] + payment_method_type = "AmericanExpress" +[[aci.debit]] + payment_method_type = "JCB" +[[aci.debit]] + payment_method_type = "DinersClub" +[[aci.debit]] + payment_method_type = "Discover" +[[aci.debit]] + payment_method_type = "CartesBancaires" +[[aci.debit]] + payment_method_type = "UnionPay" +[[aci.wallet]] + payment_method_type = "ali_pay" +[[aci.wallet]] + payment_method_type = "mb_way" +[[aci.bank_redirect]] + payment_method_type = "ideal" +[[aci.bank_redirect]] + payment_method_type = "giropay" +[[aci.bank_redirect]] + payment_method_type = "sofort" +[[aci.bank_redirect]] + payment_method_type = "eps" +[[aci.bank_redirect]] + payment_method_type = "przelewy24" +[[aci.bank_redirect]] + payment_method_type = "trustly" +[[aci.bank_redirect]] + payment_method_type = "interac" [aci.connector_auth.BodyKey] api_key="API Key" key1="Entity ID" [aci.connector_webhook_details] merchant_secret="Source verification key" -[adyen] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","affirm","afterpay_clearpay","pay_bright","walley", "alma", "atome"] -bank_debit=["ach","bacs","sepa"] -bank_redirect=["ideal","giropay","sofort","eps","blik","przelewy24","trustly","online_banking_czech_republic","online_banking_finland","online_banking_poland","online_banking_slovakia","bancontact_card", "online_banking_fpx", "online_banking_thailand", "bizum", "open_banking_uk"] -wallet = ["apple_pay","google_pay","paypal","we_chat_pay","ali_pay","mb_way", "ali_pay_hk", "go_pay", "kakao_pay", "twint", "gcash", "vipps", "momo", "dana", "swish", "touch_n_go"] -bank_transfer = ["permata_bank_transfer", "bca_bank_transfer", "bni_va", "bri_va", "cimb_va", "danamon_va", "mandiri_va"] -voucher = ["boleto", "alfamart", "indomaret", "oxxo", "seven_eleven", "lawson", "mini_stop", "family_mart", "seicomart", "pay_easy"] -gift_card = ["pay_safe_card", "givex"] -card_redirect = ["benefit", "knet", "momo_atm"] +[adyen] +[[adyen.credit]] + payment_method_type = "Mastercard" +[[adyen.credit]] + payment_method_type = "Visa" +[[adyen.credit]] + payment_method_type = "Interac" +[[adyen.credit]] + payment_method_type = "AmericanExpress" +[[adyen.credit]] + payment_method_type = "JCB" +[[adyen.credit]] + payment_method_type = "DinersClub" +[[adyen.credit]] + payment_method_type = "Discover" +[[adyen.credit]] + payment_method_type = "CartesBancaires" +[[adyen.credit]] + payment_method_type = "UnionPay" +[[adyen.debit]] + payment_method_type = "Mastercard" +[[adyen.debit]] + payment_method_type = "Visa" +[[adyen.debit]] + payment_method_type = "Interac" +[[adyen.debit]] + payment_method_type = "AmericanExpress" +[[adyen.debit]] + payment_method_type = "JCB" +[[adyen.debit]] + payment_method_type = "DinersClub" +[[adyen.debit]] + payment_method_type = "Discover" +[[adyen.debit]] + payment_method_type = "CartesBancaires" +[[adyen.debit]] + payment_method_type = "UnionPay" +[[adyen.pay_later]] + payment_method_type = "klarna" +[[adyen.pay_later]] + payment_method_type = "affirm" +[[adyen.pay_later]] + payment_method_type = "afterpay_clearpay" +[[adyen.pay_later]] + payment_method_type = "pay_bright" +[[adyen.pay_later]] + payment_method_type = "walley" +[[adyen.pay_later]] + payment_method_type = "alma" +[[adyen.pay_later]] + payment_method_type = "atome" +[[adyen.bank_debit]] + payment_method_type = "ach" +[[adyen.bank_debit]] + payment_method_type = "bacs" +[[adyen.bank_debit]] + payment_method_type = "sepa" +[[adyen.bank_redirect]] + payment_method_type = "ideal" +[[adyen.bank_redirect]] + payment_method_type = "giropay" +[[adyen.bank_redirect]] + payment_method_type = "sofort" +[[adyen.bank_redirect]] + payment_method_type = "eps" +[[adyen.bank_redirect]] + payment_method_type = "blik" +[[adyen.bank_redirect]] + payment_method_type = "przelewy24" +[[adyen.bank_redirect]] + payment_method_type = "trustly" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_czech_republic" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_finland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_poland" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_slovakia" +[[adyen.bank_redirect]] + payment_method_type = "bancontact_card" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_fpx" +[[adyen.bank_redirect]] + payment_method_type = "online_banking_thailand" +[[adyen.bank_redirect]] + payment_method_type = "bizum" +[[adyen.bank_redirect]] + payment_method_type = "open_banking_uk" +[[adyen.bank_transfer]] + payment_method_type = "permata_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bca_bank_transfer" +[[adyen.bank_transfer]] + payment_method_type = "bni_va" +[[adyen.bank_transfer]] + payment_method_type = "bri_va" +[[adyen.bank_transfer]] + payment_method_type = "cimb_va" +[[adyen.bank_transfer]] + payment_method_type = "danamon_va" +[[adyen.bank_transfer]] + payment_method_type = "mandiri_va" +[[adyen.wallet]] + payment_method_type = "apple_pay" +[[adyen.wallet]] + payment_method_type = "google_pay" +[[adyen.wallet]] + payment_method_type = "paypal" +[[adyen.wallet]] + payment_method_type = "we_chat_pay" +[[adyen.wallet]] + payment_method_type = "ali_pay" +[[adyen.wallet]] + payment_method_type = "mb_way" +[[adyen.wallet]] + payment_method_type = "ali_pay_hk" +[[adyen.wallet]] + payment_method_type = "go_pay" +[[adyen.wallet]] + payment_method_type = "kakao_pay" +[[adyen.wallet]] + payment_method_type = "twint" +[[adyen.wallet]] + payment_method_type = "gcash" +[[adyen.wallet]] + payment_method_type = "vipps" +[[adyen.wallet]] + payment_method_type = "dana" +[[adyen.wallet]] + payment_method_type = "momo" +[[adyen.wallet]] + payment_method_type = "swish" +[[adyen.wallet]] + payment_method_type = "touch_n_go" +[[adyen.voucher]] + payment_method_type = "boleto" +[[adyen.voucher]] + payment_method_type = "alfamart" +[[adyen.voucher]] + payment_method_type = "indomaret" +[[adyen.voucher]] + payment_method_type = "oxxo" +[[adyen.voucher]] + payment_method_type = "seven_eleven" +[[adyen.voucher]] + payment_method_type = "lawson" +[[adyen.voucher]] + payment_method_type = "mini_stop" +[[adyen.voucher]] + payment_method_type = "family_mart" +[[adyen.voucher]] + payment_method_type = "seicomart" +[[adyen.voucher]] + payment_method_type = "pay_easy" +[[adyen.gift_card]] + payment_method_type = "pay_safe_card" +[[adyen.gift_card]] + payment_method_type = "givex" +[[adyen.card_redirect]] + payment_method_type = "benefit" +[[adyen.card_redirect]] + payment_method_type = "knet" +[[adyen.card_redirect]] + payment_method_type = "momo_atm" [adyen.connector_auth.BodyKey] api_key="Adyen API Key" key1="Adyen Account Id" [adyen.connector_webhook_details] merchant_secret="Source verification key" - [adyen.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" @@ -45,24 +247,93 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" - - [airwallex] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay"] -body_type="BodyKey" +[[airwallex.credit]] + payment_method_type = "Mastercard" +[[airwallex.credit]] + payment_method_type = "Visa" +[[airwallex.credit]] + payment_method_type = "Interac" +[[airwallex.credit]] + payment_method_type = "AmericanExpress" +[[airwallex.credit]] + payment_method_type = "JCB" +[[airwallex.credit]] + payment_method_type = "DinersClub" +[[airwallex.credit]] + payment_method_type = "Discover" +[[airwallex.credit]] + payment_method_type = "CartesBancaires" +[[airwallex.credit]] + payment_method_type = "UnionPay" +[[airwallex.debit]] + payment_method_type = "Mastercard" +[[airwallex.debit]] + payment_method_type = "Visa" +[[airwallex.debit]] + payment_method_type = "Interac" +[[airwallex.debit]] + payment_method_type = "AmericanExpress" +[[airwallex.debit]] + payment_method_type = "JCB" +[[airwallex.debit]] + payment_method_type = "DinersClub" +[[airwallex.debit]] + payment_method_type = "Discover" +[[airwallex.debit]] + payment_method_type = "CartesBancaires" +[[airwallex.debit]] + payment_method_type = "UnionPay" +[[airwallex.wallet]] + payment_method_type = "google_pay" [airwallex.connector_auth.BodyKey] api_key="API Key" key1="Client ID" [airwallex.connector_webhook_details] merchant_secret="Source verification key" + [authorizedotnet] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","paypal"] -body_type="BodyKey" +[[authorizedotnet.credit]] + payment_method_type = "Mastercard" +[[authorizedotnet.credit]] + payment_method_type = "Visa" +[[authorizedotnet.credit]] + payment_method_type = "Interac" +[[authorizedotnet.credit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.credit]] + payment_method_type = "JCB" +[[authorizedotnet.credit]] + payment_method_type = "DinersClub" +[[authorizedotnet.credit]] + payment_method_type = "Discover" +[[authorizedotnet.credit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.credit]] + payment_method_type = "UnionPay" +[[authorizedotnet.debit]] + payment_method_type = "Mastercard" +[[authorizedotnet.debit]] + payment_method_type = "Visa" +[[authorizedotnet.debit]] + payment_method_type = "Interac" +[[authorizedotnet.debit]] + payment_method_type = "AmericanExpress" +[[authorizedotnet.debit]] + payment_method_type = "JCB" +[[authorizedotnet.debit]] + payment_method_type = "DinersClub" +[[authorizedotnet.debit]] + payment_method_type = "Discover" +[[authorizedotnet.debit]] + payment_method_type = "CartesBancaires" +[[authorizedotnet.debit]] + payment_method_type = "UnionPay" +[[authorizedotnet.wallet]] + payment_method_type = "google_pay" +[[authorizedotnet.wallet]] + payment_method_type = "paypal" [authorizedotnet.connector_auth.BodyKey] api_key="API Login ID" key1="Transaction Key" @@ -73,17 +344,178 @@ merchant_id="Google Pay Merchant ID" [authorizedotnet.connector_webhook_details] merchant_secret="Source verification key" +[bambora] +[[bambora.credit]] + payment_method_type = "Mastercard" +[[bambora.credit]] + payment_method_type = "Visa" +[[bambora.credit]] + payment_method_type = "Interac" +[[bambora.credit]] + payment_method_type = "AmericanExpress" +[[bambora.credit]] + payment_method_type = "JCB" +[[bambora.credit]] + payment_method_type = "DinersClub" +[[bambora.credit]] + payment_method_type = "Discover" +[[bambora.credit]] + payment_method_type = "CartesBancaires" +[[bambora.credit]] + payment_method_type = "UnionPay" +[[bambora.debit]] + payment_method_type = "Mastercard" +[[bambora.debit]] + payment_method_type = "Visa" +[[bambora.debit]] + payment_method_type = "Interac" +[[bambora.debit]] + payment_method_type = "AmericanExpress" +[[bambora.debit]] + payment_method_type = "JCB" +[[bambora.debit]] + payment_method_type = "DinersClub" +[[bambora.debit]] + payment_method_type = "Discover" +[[bambora.debit]] + payment_method_type = "CartesBancaires" +[[bambora.debit]] + payment_method_type = "UnionPay" +[[bambora.wallet]] + payment_method_type = "apple_pay" +[[bambora.wallet]] + payment_method_type = "paypal" +[bambora.connector_auth.BodyKey] +api_key="Passcode" +key1="Merchant Id" +[bambora.connector_webhook_details] +merchant_secret="Source verification key" +[bambora.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bambora.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica] +[[bankofamerica.credit]] + payment_method_type = "Mastercard" +[[bankofamerica.credit]] + payment_method_type = "Visa" +[[bankofamerica.credit]] + payment_method_type = "Interac" +[[bankofamerica.credit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.credit]] + payment_method_type = "JCB" +[[bankofamerica.credit]] + payment_method_type = "DinersClub" +[[bankofamerica.credit]] + payment_method_type = "Discover" +[[bankofamerica.credit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.credit]] + payment_method_type = "UnionPay" +[[bankofamerica.debit]] + payment_method_type = "Mastercard" +[[bankofamerica.debit]] + payment_method_type = "Visa" +[[bankofamerica.debit]] + payment_method_type = "Interac" +[[bankofamerica.debit]] + payment_method_type = "AmericanExpress" +[[bankofamerica.debit]] + payment_method_type = "JCB" +[[bankofamerica.debit]] + payment_method_type = "DinersClub" +[[bankofamerica.debit]] + payment_method_type = "Discover" +[[bankofamerica.debit]] + payment_method_type = "CartesBancaires" +[[bankofamerica.debit]] + payment_method_type = "UnionPay" +[[bankofamerica.wallet]] + payment_method_type = "apple_pay" +[[bankofamerica.wallet]] + payment_method_type = "google_pay" +[bankofamerica.connector_auth.SignatureKey] +api_key="Key" +key1="Merchant ID" +api_secret="Shared Secret" +[bankofamerica.connector_webhook_details] +merchant_secret="Source verification key" + +[bankofamerica.metadata.apple_pay.session_token_data] +certificate="Merchant Certificate (Base64 Encoded)" +certificate_keys="Merchant PrivateKey (Base64 Encoded)" +merchant_identifier="Apple Merchant Identifier" +display_name="Display Name" +initiative="Domain" +initiative_context="Domain Name" +[bankofamerica.metadata.apple_pay.payment_request_data] +supported_networks=["visa","masterCard","amex","discover"] +merchant_capabilities=["supports3DS"] +label="apple" + +[bankofamerica.metadata.google_pay] +merchant_name="Google Pay Merchant Name" +gateway_merchant_id="Google Pay Merchant Key" +merchant_id="Google Pay Merchant ID" + [bitpay] -crypto = ["crypto_currency"] +[[bitpay.crypto]] + payment_method_type = "crypto_currency" [bitpay.connector_auth.HeaderKey] api_key="API Key" [bitpay.connector_webhook_details] merchant_secret="Source verification key" [bluesnap] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","apple_pay"] +[[bluesnap.credit]] + payment_method_type = "Mastercard" +[[bluesnap.credit]] + payment_method_type = "Visa" +[[bluesnap.credit]] + payment_method_type = "Interac" +[[bluesnap.credit]] + payment_method_type = "AmericanExpress" +[[bluesnap.credit]] + payment_method_type = "JCB" +[[bluesnap.credit]] + payment_method_type = "DinersClub" +[[bluesnap.credit]] + payment_method_type = "Discover" +[[bluesnap.credit]] + payment_method_type = "CartesBancaires" +[[bluesnap.credit]] + payment_method_type = "UnionPay" +[[bluesnap.debit]] + payment_method_type = "Mastercard" +[[bluesnap.debit]] + payment_method_type = "Visa" +[[bluesnap.debit]] + payment_method_type = "Interac" +[[bluesnap.debit]] + payment_method_type = "AmericanExpress" +[[bluesnap.debit]] + payment_method_type = "JCB" +[[bluesnap.debit]] + payment_method_type = "DinersClub" +[[bluesnap.debit]] + payment_method_type = "Discover" +[[bluesnap.debit]] + payment_method_type = "CartesBancaires" +[[bluesnap.debit]] + payment_method_type = "UnionPay" +[[bluesnap.wallet]] + payment_method_type = "google_pay" +[[bluesnap.wallet]] + payment_method_type = "apple_pay" [bluesnap.connector_auth.BodyKey] api_key="Password" key1="Username" @@ -109,25 +541,148 @@ label="apple" [bluesnap.metadata] merchant_id="Merchant Id" +[boku] +[[boku.wallet]] + payment_method_type = "dana" +[[boku.wallet]] + payment_method_type = "gcash" +[[boku.wallet]] + payment_method_type = "go_pay" +[[boku.wallet]] + payment_method_type = "kakao_pay" +[[boku.wallet]] + payment_method_type = "momo" +[boku.connector_auth.BodyKey] +api_key="API KEY" +key1= "MERCHANT ID" +[boku.connector_webhook_details] +merchant_secret="Source verification key" + [braintree] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[braintree.credit]] + payment_method_type = "Mastercard" +[[braintree.credit]] + payment_method_type = "Visa" +[[braintree.credit]] + payment_method_type = "Interac" +[[braintree.credit]] + payment_method_type = "AmericanExpress" +[[braintree.credit]] + payment_method_type = "JCB" +[[braintree.credit]] + payment_method_type = "DinersClub" +[[braintree.credit]] + payment_method_type = "Discover" +[[braintree.credit]] + payment_method_type = "CartesBancaires" +[[braintree.credit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "Mastercard" +[[braintree.debit]] + payment_method_type = "Visa" +[[braintree.debit]] + payment_method_type = "Interac" +[[braintree.debit]] + payment_method_type = "AmericanExpress" +[[braintree.debit]] + payment_method_type = "JCB" +[[braintree.debit]] + payment_method_type = "DinersClub" +[[braintree.debit]] + payment_method_type = "Discover" +[[braintree.debit]] + payment_method_type = "CartesBancaires" +[[braintree.debit]] + payment_method_type = "UnionPay" +[[braintree.debit]] + payment_method_type = "UnionPay" +[braintree.connector_webhook_details] +merchant_secret="Source verification key" + [braintree.connector_auth.SignatureKey] api_key="Public Key" key1="Merchant Id" api_secret="Private Key" -[braintree.connector_webhook_details] -merchant_secret="Source verification key" [braintree.metadata] merchant_account_id="Merchant Account Id" merchant_config_currency="Currency" +[cashtocode] +[[cashtocode.reward]] + payment_method_type = "classic" +[[cashtocode.reward]] + payment_method_type = "evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" + [checkout] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay","paypal"] +[[checkout.credit]] + payment_method_type = "Mastercard" +[[checkout.credit]] + payment_method_type = "Visa" +[[checkout.credit]] + payment_method_type = "Interac" +[[checkout.credit]] + payment_method_type = "AmericanExpress" +[[checkout.credit]] + payment_method_type = "JCB" +[[checkout.credit]] + payment_method_type = "DinersClub" +[[checkout.credit]] + payment_method_type = "Discover" +[[checkout.credit]] + payment_method_type = "CartesBancaires" +[[checkout.credit]] + payment_method_type = "UnionPay" +[[checkout.debit]] + payment_method_type = "Mastercard" +[[checkout.debit]] + payment_method_type = "Visa" +[[checkout.debit]] + payment_method_type = "Interac" +[[checkout.debit]] + payment_method_type = "AmericanExpress" +[[checkout.debit]] + payment_method_type = "JCB" +[[checkout.debit]] + payment_method_type = "DinersClub" +[[checkout.debit]] + payment_method_type = "Discover" +[[checkout.debit]] + payment_method_type = "CartesBancaires" +[[checkout.debit]] + payment_method_type = "UnionPay" +[[checkout.wallet]] + payment_method_type = "apple_pay" +[[checkout.wallet]] + payment_method_type = "google_pay" +[[checkout.wallet]] + payment_method_type = "paypal" [checkout.connector_auth.SignatureKey] api_key="Checkout API Public Key" key1="Processing Channel ID" @@ -152,19 +707,64 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" - - [coinbase] -crypto = ["crypto_currency"] +[[coinbase.crypto]] + payment_method_type = "crypto_currency" [coinbase.connector_auth.HeaderKey] api_key="API Key" [coinbase.connector_webhook_details] merchant_secret="Source verification key" +[cryptopay] +[[cryptopay.crypto]] + payment_method_type = "crypto_currency" +[cryptopay.connector_auth.BodyKey] +api_key="API Key" +key1="Secret Key" +[cryptopay.connector_webhook_details] +merchant_secret="Source verification key" + [cybersource] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] +[[cybersource.credit]] + payment_method_type = "Mastercard" +[[cybersource.credit]] + payment_method_type = "Visa" +[[cybersource.credit]] + payment_method_type = "Interac" +[[cybersource.credit]] + payment_method_type = "AmericanExpress" +[[cybersource.credit]] + payment_method_type = "JCB" +[[cybersource.credit]] + payment_method_type = "DinersClub" +[[cybersource.credit]] + payment_method_type = "Discover" +[[cybersource.credit]] + payment_method_type = "CartesBancaires" +[[cybersource.credit]] + payment_method_type = "UnionPay" +[[cybersource.debit]] + payment_method_type = "Mastercard" +[[cybersource.debit]] + payment_method_type = "Visa" +[[cybersource.debit]] + payment_method_type = "Interac" +[[cybersource.debit]] + payment_method_type = "AmericanExpress" +[[cybersource.debit]] + payment_method_type = "JCB" +[[cybersource.debit]] + payment_method_type = "DinersClub" +[[cybersource.debit]] + payment_method_type = "Discover" +[[cybersource.debit]] + payment_method_type = "CartesBancaires" +[[cybersource.debit]] + payment_method_type = "UnionPay" +[[cybersource.wallet]] + payment_method_type = "apple_pay" +[[cybersource.wallet]] + payment_method_type = "google_pay" [cybersource.connector_auth.SignatureKey] api_key="Key" key1="Merchant ID" @@ -189,63 +789,43 @@ merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" -[helcim] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[helcim.connector_auth.HeaderKey] -api_key="Api Key" -[helcim.connector_webhook_details] -merchant_secret="Source verification key" - -[iatapay] -upi=["upi_collect"] -[iatapay.connector_auth.SignatureKey] -api_key="Client ID" -key1="Airline ID" -api_secret="Client Secret" -[iatapay.connector_webhook_details] -merchant_secret="Source verification key" - -[opennode] -crypto = ["crypto_currency"] -[opennode.connector_auth.HeaderKey] -api_key="API Key" -[opennode.connector_webhook_details] -merchant_secret="Source verification key" - -[bambora] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","paypal"] -[bambora.connector_auth.BodyKey] -api_key="Passcode" -key1="Merchant Id" -[bambora.connector_webhook_details] -merchant_secret="Source verification key" - -[bambora.metadata.apple_pay.session_token_data] -certificate="Merchant Certificate (Base64 Encoded)" -certificate_keys="Merchant PrivateKey (Base64 Encoded)" -merchant_identifier="Apple Merchant Identifier" -display_name="Display Name" -initiative="Domain" -initiative_context="Domain Name" -[bambora.metadata.apple_pay.payment_request_data] -supported_networks=["visa","masterCard","amex","discover"] -merchant_capabilities=["supports3DS"] -label="apple" - -[boku] -wallet = ["dana","gcash","go_pay","kakao_pay","momo"] -[boku.connector_auth.BodyKey] -api_key="API KEY" -key1= "MERCHANT ID" -[boku.connector_webhook_details] -merchant_secret="Source verification key" - [dlocal] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[dlocal.credit]] + payment_method_type = "Mastercard" +[[dlocal.credit]] + payment_method_type = "Visa" +[[dlocal.credit]] + payment_method_type = "Interac" +[[dlocal.credit]] + payment_method_type = "AmericanExpress" +[[dlocal.credit]] + payment_method_type = "JCB" +[[dlocal.credit]] + payment_method_type = "DinersClub" +[[dlocal.credit]] + payment_method_type = "Discover" +[[dlocal.credit]] + payment_method_type = "CartesBancaires" +[[dlocal.credit]] + payment_method_type = "UnionPay" +[[dlocal.debit]] + payment_method_type = "Mastercard" +[[dlocal.debit]] + payment_method_type = "Visa" +[[dlocal.debit]] + payment_method_type = "Interac" +[[dlocal.debit]] + payment_method_type = "AmericanExpress" +[[dlocal.debit]] + payment_method_type = "JCB" +[[dlocal.debit]] + payment_method_type = "DinersClub" +[[dlocal.debit]] + payment_method_type = "Discover" +[[dlocal.debit]] + payment_method_type = "CartesBancaires" +[[dlocal.debit]] + payment_method_type = "UnionPay" [dlocal.connector_auth.SignatureKey] api_key="X Login" key1="X Trans Key" @@ -254,20 +834,88 @@ api_secret="Secret Key" merchant_secret="Source verification key" [fiserv] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[fiserv.credit]] + payment_method_type = "Mastercard" +[[fiserv.credit]] + payment_method_type = "Visa" +[[fiserv.credit]] + payment_method_type = "Interac" +[[fiserv.credit]] + payment_method_type = "AmericanExpress" +[[fiserv.credit]] + payment_method_type = "JCB" +[[fiserv.credit]] + payment_method_type = "DinersClub" +[[fiserv.credit]] + payment_method_type = "Discover" +[[fiserv.credit]] + payment_method_type = "CartesBancaires" +[[fiserv.credit]] + payment_method_type = "UnionPay" +[[fiserv.debit]] + payment_method_type = "Mastercard" +[[fiserv.debit]] + payment_method_type = "Visa" +[[fiserv.debit]] + payment_method_type = "Interac" +[[fiserv.debit]] + payment_method_type = "AmericanExpress" +[[fiserv.debit]] + payment_method_type = "JCB" +[[fiserv.debit]] + payment_method_type = "DinersClub" +[[fiserv.debit]] + payment_method_type = "Discover" +[[fiserv.debit]] + payment_method_type = "CartesBancaires" +[[fiserv.debit]] + payment_method_type = "UnionPay" [fiserv.connector_auth.SignatureKey] api_key="API Key" key1="Merchant ID" api_secret="API Secret" -[fiserv.connector_webhook_details] -merchant_secret="Source verification key" [fiserv.metadata] terminal_id="Terminal ID" +[fiserv.connector_webhook_details] +merchant_secret="Source verification key" [forte] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[forte.credit]] + payment_method_type = "Mastercard" +[[forte.credit]] + payment_method_type = "Visa" +[[forte.credit]] + payment_method_type = "Interac" +[[forte.credit]] + payment_method_type = "AmericanExpress" +[[forte.credit]] + payment_method_type = "JCB" +[[forte.credit]] + payment_method_type = "DinersClub" +[[forte.credit]] + payment_method_type = "Discover" +[[forte.credit]] + payment_method_type = "CartesBancaires" +[[forte.credit]] + payment_method_type = "UnionPay" +[[forte.debit]] + payment_method_type = "Mastercard" +[[forte.debit]] + payment_method_type = "Visa" +[[forte.debit]] + payment_method_type = "Interac" +[[forte.debit]] + payment_method_type = "AmericanExpress" +[[forte.debit]] + payment_method_type = "JCB" +[[forte.debit]] + payment_method_type = "DinersClub" +[[forte.debit]] + payment_method_type = "Discover" +[[forte.debit]] + payment_method_type = "CartesBancaires" +[[forte.debit]] + payment_method_type = "UnionPay" [forte.connector_auth.MultiAuthKey] api_key="API Access ID" key1="Organization ID" @@ -277,10 +925,54 @@ key2="Location ID" merchant_secret="Source verification key" [globalpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["google_pay","paypal"] +[[globalpay.credit]] + payment_method_type = "Mastercard" +[[globalpay.credit]] + payment_method_type = "Visa" +[[globalpay.credit]] + payment_method_type = "Interac" +[[globalpay.credit]] + payment_method_type = "AmericanExpress" +[[globalpay.credit]] + payment_method_type = "JCB" +[[globalpay.credit]] + payment_method_type = "DinersClub" +[[globalpay.credit]] + payment_method_type = "Discover" +[[globalpay.credit]] + payment_method_type = "CartesBancaires" +[[globalpay.credit]] + payment_method_type = "UnionPay" +[[globalpay.debit]] + payment_method_type = "Mastercard" +[[globalpay.debit]] + payment_method_type = "Visa" +[[globalpay.debit]] + payment_method_type = "Interac" +[[globalpay.debit]] + payment_method_type = "AmericanExpress" +[[globalpay.debit]] + payment_method_type = "JCB" +[[globalpay.debit]] + payment_method_type = "DinersClub" +[[globalpay.debit]] + payment_method_type = "Discover" +[[globalpay.debit]] + payment_method_type = "CartesBancaires" +[[globalpay.debit]] + payment_method_type = "UnionPay" +[[globalpay.bank_redirect]] + payment_method_type = "ideal" +[[globalpay.bank_redirect]] + payment_method_type = "giropay" +[[globalpay.bank_redirect]] + payment_method_type = "sofort" +[[globalpay.bank_redirect]] + payment_method_type = "eps" +[[globalpay.wallet]] + payment_method_type = "google_pay" +[[globalpay.wallet]] + payment_method_type = "paypal" [globalpay.connector_auth.BodyKey] api_key="Global App Key" key1="Global App ID" @@ -288,24 +980,103 @@ key1="Global App ID" account_name="Account Name" [globalpay.connector_webhook_details] merchant_secret="Source verification key" - [globalpay.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" +[globepay] +[[globepay.wallet]] + payment_method_type = "we_chat_pay" +[[globepay.wallet]] + payment_method_type = "ali_pay" +[globepay.connector_auth.BodyKey] +api_key="Partner Code" +key1="Credential Code" +[globepay.connector_webhook_details] +merchant_secret="Source verification key" + +[gocardless] +[[gocardless.bank_debit]] + payment_method_type = "ach" +[[gocardless.bank_debit]] + payment_method_type = "becs" +[[gocardless.bank_debit]] + payment_method_type = "sepa" +[gocardless.connector_auth.HeaderKey] +api_key="Access Token" +[gocardless.connector_webhook_details] +merchant_secret="Source verification key" + +[iatapay] +[[iatapay.upi]] + payment_method_type = "upi_collect" +[iatapay.connector_auth.SignatureKey] +api_key="Client ID" +key1="Airline ID" +api_secret="Client Secret" +[iatapay.connector_webhook_details] +merchant_secret="Source verification key" + [klarna] -pay_later=["klarna"] +[[klarna.pay_later]] + payment_method_type = "klarna" [klarna.connector_auth.HeaderKey] api_key="Klarna API Key" [klarna.connector_webhook_details] merchant_secret="Source verification key" [mollie] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps","przelewy24","bancontact_card"] -wallet = ["paypal"] +[[mollie.credit]] + payment_method_type = "Mastercard" +[[mollie.credit]] + payment_method_type = "Visa" +[[mollie.credit]] + payment_method_type = "Interac" +[[mollie.credit]] + payment_method_type = "AmericanExpress" +[[mollie.credit]] + payment_method_type = "JCB" +[[mollie.credit]] + payment_method_type = "DinersClub" +[[mollie.credit]] + payment_method_type = "Discover" +[[mollie.credit]] + payment_method_type = "CartesBancaires" +[[mollie.credit]] + payment_method_type = "UnionPay" +[[mollie.debit]] + payment_method_type = "Mastercard" +[[mollie.debit]] + payment_method_type = "Visa" +[[mollie.debit]] + payment_method_type = "Interac" +[[mollie.debit]] + payment_method_type = "AmericanExpress" +[[mollie.debit]] + payment_method_type = "JCB" +[[mollie.debit]] + payment_method_type = "DinersClub" +[[mollie.debit]] + payment_method_type = "Discover" +[[mollie.debit]] + payment_method_type = "CartesBancaires" +[[mollie.debit]] + payment_method_type = "UnionPay" +[[mollie.bank_redirect]] + payment_method_type = "ideal" +[[mollie.bank_redirect]] + payment_method_type = "giropay" +[[mollie.bank_redirect]] + payment_method_type = "sofort" +[[mollie.bank_redirect]] + payment_method_type = "eps" +[[mollie.bank_redirect]] + payment_method_type = "przelewy24" +[[mollie.bank_redirect]] + payment_method_type = "bancontact_card" +[[mollie.wallet]] + payment_method_type = "paypal" [mollie.connector_auth.BodyKey] api_key="API Key" key1="Profile Token" @@ -313,30 +1084,109 @@ key1="Profile Token" merchant_secret="Source verification key" [multisafepay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","paypal"] +[[multisafepay.credit]] + payment_method_type = "Mastercard" +[[multisafepay.credit]] + payment_method_type = "Visa" +[[multisafepay.credit]] + payment_method_type = "Interac" +[[multisafepay.credit]] + payment_method_type = "AmericanExpress" +[[multisafepay.credit]] + payment_method_type = "JCB" +[[multisafepay.credit]] + payment_method_type = "DinersClub" +[[multisafepay.credit]] + payment_method_type = "Discover" +[[multisafepay.credit]] + payment_method_type = "CartesBancaires" +[[multisafepay.credit]] + payment_method_type = "UnionPay" +[[multisafepay.debit]] + payment_method_type = "Mastercard" +[[multisafepay.debit]] + payment_method_type = "Visa" +[[multisafepay.debit]] + payment_method_type = "Interac" +[[multisafepay.debit]] + payment_method_type = "AmericanExpress" +[[multisafepay.debit]] + payment_method_type = "JCB" +[[multisafepay.debit]] + payment_method_type = "DinersClub" +[[multisafepay.debit]] + payment_method_type = "Discover" +[[multisafepay.debit]] + payment_method_type = "CartesBancaires" +[[multisafepay.debit]] + payment_method_type = "UnionPay" +[[multisafepay.wallet]] + payment_method_type = "google_pay" +[[multisafepay.wallet]] + payment_method_type = "paypal" [multisafepay.connector_auth.HeaderKey] api_key="Enter API Key" [multisafepay.connector_webhook_details] merchant_secret="Source verification key" - [multisafepay.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" [nexinets] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["apple_pay","paypal"] +[[nexinets.credit]] + payment_method_type = "Mastercard" +[[nexinets.credit]] + payment_method_type = "Visa" +[[nexinets.credit]] + payment_method_type = "Interac" +[[nexinets.credit]] + payment_method_type = "AmericanExpress" +[[nexinets.credit]] + payment_method_type = "JCB" +[[nexinets.credit]] + payment_method_type = "DinersClub" +[[nexinets.credit]] + payment_method_type = "Discover" +[[nexinets.credit]] + payment_method_type = "CartesBancaires" +[[nexinets.credit]] + payment_method_type = "UnionPay" +[[nexinets.debit]] + payment_method_type = "Mastercard" +[[nexinets.debit]] + payment_method_type = "Visa" +[[nexinets.debit]] + payment_method_type = "Interac" +[[nexinets.debit]] + payment_method_type = "AmericanExpress" +[[nexinets.debit]] + payment_method_type = "JCB" +[[nexinets.debit]] + payment_method_type = "DinersClub" +[[nexinets.debit]] + payment_method_type = "Discover" +[[nexinets.debit]] + payment_method_type = "CartesBancaires" +[[nexinets.debit]] + payment_method_type = "UnionPay" +[[nexinets.bank_redirect]] + payment_method_type = "ideal" +[[nexinets.bank_redirect]] + payment_method_type = "giropay" +[[nexinets.bank_redirect]] + payment_method_type = "sofort" +[[nexinets.bank_redirect]] + payment_method_type = "eps" +[[nexinets.wallet]] + payment_method_type = "apple_pay" +[[nexinets.wallet]] + payment_method_type = "paypal" [nexinets.connector_auth.BodyKey] api_key="API Key" key1="Merchant ID" [nexinets.connector_webhook_details] merchant_secret="Source verification key" - [nexinets.metadata.apple_pay.session_token_data] certificate="Merchant Certificate (Base64 Encoded)" certificate_keys="Merchant PrivateKey (Base64 Encoded)" @@ -350,9 +1200,48 @@ merchant_capabilities=["supports3DS"] label="apple" [nmi] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] +[[nmi.credit]] + payment_method_type = "Mastercard" +[[nmi.credit]] + payment_method_type = "Visa" +[[nmi.credit]] + payment_method_type = "Interac" +[[nmi.credit]] + payment_method_type = "AmericanExpress" +[[nmi.credit]] + payment_method_type = "JCB" +[[nmi.credit]] + payment_method_type = "DinersClub" +[[nmi.credit]] + payment_method_type = "Discover" +[[nmi.credit]] + payment_method_type = "CartesBancaires" +[[nmi.credit]] + payment_method_type = "UnionPay" +[[nmi.debit]] + payment_method_type = "Mastercard" +[[nmi.debit]] + payment_method_type = "Visa" +[[nmi.debit]] + payment_method_type = "Interac" +[[nmi.debit]] + payment_method_type = "AmericanExpress" +[[nmi.debit]] + payment_method_type = "JCB" +[[nmi.debit]] + payment_method_type = "DinersClub" +[[nmi.debit]] + payment_method_type = "Discover" +[[nmi.debit]] + payment_method_type = "CartesBancaires" +[[nmi.debit]] + payment_method_type = "UnionPay" +[[nmi.bank_redirect]] + payment_method_type = "ideal" +[[nmi.wallet]] + payment_method_type = "apple_pay" +[[nmi.wallet]] + payment_method_type = "google_pay" [nmi.connector_auth.HeaderKey] api_key="API Key" [nmi.connector_webhook_details] @@ -376,9 +1265,48 @@ merchant_capabilities=["supports3DS"] label="apple" [noon] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay","paypal"] +[[noon.credit]] + payment_method_type = "Mastercard" +[[noon.credit]] + payment_method_type = "Visa" +[[noon.credit]] + payment_method_type = "Interac" +[[noon.credit]] + payment_method_type = "AmericanExpress" +[[noon.credit]] + payment_method_type = "JCB" +[[noon.credit]] + payment_method_type = "DinersClub" +[[noon.credit]] + payment_method_type = "Discover" +[[noon.credit]] + payment_method_type = "CartesBancaires" +[[noon.credit]] + payment_method_type = "UnionPay" +[[noon.debit]] + payment_method_type = "Mastercard" +[[noon.debit]] + payment_method_type = "Visa" +[[noon.debit]] + payment_method_type = "Interac" +[[noon.debit]] + payment_method_type = "AmericanExpress" +[[noon.debit]] + payment_method_type = "JCB" +[[noon.debit]] + payment_method_type = "DinersClub" +[[noon.debit]] + payment_method_type = "Discover" +[[noon.debit]] + payment_method_type = "CartesBancaires" +[[noon.debit]] + payment_method_type = "UnionPay" +[[noon.wallet]] + payment_method_type = "apple_pay" +[[noon.wallet]] + payment_method_type = "google_pay" +[[noon.wallet]] + payment_method_type = "paypal" [noon.connector_auth.SignatureKey] api_key="API Key" key1="Business Identifier" @@ -404,15 +1332,67 @@ merchant_capabilities=["supports3DS"] label="apple" [nuvei] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","afterpay_clearpay"] -bank_redirect=["ideal","giropay","sofort","eps"] -wallet = ["apple_pay","google_pay","paypal"] +[[nuvei.credit]] + payment_method_type = "Mastercard" +[[nuvei.credit]] + payment_method_type = "Visa" +[[nuvei.credit]] + payment_method_type = "Interac" +[[nuvei.credit]] + payment_method_type = "AmericanExpress" +[[nuvei.credit]] + payment_method_type = "JCB" +[[nuvei.credit]] + payment_method_type = "DinersClub" +[[nuvei.credit]] + payment_method_type = "Discover" +[[nuvei.credit]] + payment_method_type = "CartesBancaires" +[[nuvei.credit]] + payment_method_type = "UnionPay" +[[nuvei.debit]] + payment_method_type = "Mastercard" +[[nuvei.debit]] + payment_method_type = "Visa" +[[nuvei.debit]] + payment_method_type = "Interac" +[[nuvei.debit]] + payment_method_type = "AmericanExpress" +[[nuvei.debit]] + payment_method_type = "JCB" +[[nuvei.debit]] + payment_method_type = "DinersClub" +[[nuvei.debit]] + payment_method_type = "Discover" +[[nuvei.debit]] + payment_method_type = "CartesBancaires" +[[nuvei.debit]] + payment_method_type = "UnionPay" +[[nuvei.pay_later]] + payment_method_type = "klarna" +[[nuvei.pay_later]] + payment_method_type = "afterpay_clearpay" +[[nuvei.bank_redirect]] + payment_method_type = "ideal" +[[nuvei.bank_redirect]] + payment_method_type = "giropay" +[[nuvei.bank_redirect]] + payment_method_type = "sofort" +[[nuvei.bank_redirect]] + payment_method_type = "eps" +[[nuvei.wallet]] + payment_method_type = "apple_pay" +[[nuvei.wallet]] + payment_method_type = "google_pay" +[[nuvei.wallet]] + payment_method_type = "paypal" [nuvei.connector_auth.SignatureKey] api_key="Merchant ID" key1="Merchant Site ID" api_secret="Merchant Secret" +[nuvei.connector_webhook_details] +merchant_secret="Source verification key" + [nuvei.metadata.google_pay] merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" @@ -430,11 +1410,114 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" + +[opennode] +[[opennode.crypto]] + payment_method_type = "crypto_currency" +[opennode.connector_auth.HeaderKey] +api_key="API Key" +[opennode.connector_webhook_details] +merchant_secret="Source verification key" + +[prophetpay] +[[prophetpay.card_redirect]] + payment_method_type = "card_redirect" +[prophetpay.connector_auth.SignatureKey] +api_key="Username" +key1="Token" +api_secret="Profile" + +[payme] +[[payme.credit]] + payment_method_type = "Mastercard" +[[payme.credit]] + payment_method_type = "Visa" +[[payme.credit]] + payment_method_type = "Interac" +[[payme.credit]] + payment_method_type = "AmericanExpress" +[[payme.credit]] + payment_method_type = "JCB" +[[payme.credit]] + payment_method_type = "DinersClub" +[[payme.credit]] + payment_method_type = "Discover" +[[payme.credit]] + payment_method_type = "CartesBancaires" +[[payme.credit]] + payment_method_type = "UnionPay" +[[payme.debit]] + payment_method_type = "Mastercard" +[[payme.debit]] + payment_method_type = "Visa" +[[payme.debit]] + payment_method_type = "Interac" +[[payme.debit]] + payment_method_type = "AmericanExpress" +[[payme.debit]] + payment_method_type = "JCB" +[[payme.debit]] + payment_method_type = "DinersClub" +[[payme.debit]] + payment_method_type = "Discover" +[[payme.debit]] + payment_method_type = "CartesBancaires" +[[payme.debit]] + payment_method_type = "UnionPay" +[payme.connector_auth.BodyKey] +api_key="Seller Payme Id" +key1="Payme Public Key" +[payme.connector_webhook_details] +merchant_secret="Payme Client Secret" +additional_secret="Payme Client Key" + [paypal] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["paypal"] -bank_redirect=["ideal","giropay","sofort","eps"] +[[paypal.credit]] + payment_method_type = "Mastercard" +[[paypal.credit]] + payment_method_type = "Visa" +[[paypal.credit]] + payment_method_type = "Interac" +[[paypal.credit]] + payment_method_type = "AmericanExpress" +[[paypal.credit]] + payment_method_type = "JCB" +[[paypal.credit]] + payment_method_type = "DinersClub" +[[paypal.credit]] + payment_method_type = "Discover" +[[paypal.credit]] + payment_method_type = "CartesBancaires" +[[paypal.credit]] + payment_method_type = "UnionPay" +[[paypal.debit]] + payment_method_type = "Mastercard" +[[paypal.debit]] + payment_method_type = "Visa" +[[paypal.debit]] + payment_method_type = "Interac" +[[paypal.debit]] + payment_method_type = "AmericanExpress" +[[paypal.debit]] + payment_method_type = "JCB" +[[paypal.debit]] + payment_method_type = "DinersClub" +[[paypal.debit]] + payment_method_type = "Discover" +[[paypal.debit]] + payment_method_type = "CartesBancaires" +[[paypal.debit]] + payment_method_type = "UnionPay" +[[paypal.wallet]] + payment_method_type = "paypal" +[[paypal.bank_redirect]] + payment_method_type = "ideal" +[[paypal.bank_redirect]] + payment_method_type = "giropay" +[[paypal.bank_redirect]] + payment_method_type = "sofort" +[[paypal.bank_redirect]] + payment_method_type = "eps" is_verifiable = true [paypal.connector_auth.BodyKey] api_key="Client Secret" @@ -442,10 +1525,46 @@ key1="Client ID" [paypal.connector_webhook_details] merchant_secret="Source verification key" + [payu] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay"] +[[payu.credit]] + payment_method_type = "Mastercard" +[[payu.credit]] + payment_method_type = "Visa" +[[payu.credit]] + payment_method_type = "Interac" +[[payu.credit]] + payment_method_type = "AmericanExpress" +[[payu.credit]] + payment_method_type = "JCB" +[[payu.credit]] + payment_method_type = "DinersClub" +[[payu.credit]] + payment_method_type = "Discover" +[[payu.credit]] + payment_method_type = "CartesBancaires" +[[payu.credit]] + payment_method_type = "UnionPay" +[[payu.debit]] + payment_method_type = "Mastercard" +[[payu.debit]] + payment_method_type = "Visa" +[[payu.debit]] + payment_method_type = "Interac" +[[payu.debit]] + payment_method_type = "AmericanExpress" +[[payu.debit]] + payment_method_type = "JCB" +[[payu.debit]] + payment_method_type = "DinersClub" +[[payu.debit]] + payment_method_type = "Discover" +[[payu.debit]] + payment_method_type = "CartesBancaires" +[[payu.debit]] + payment_method_type = "UnionPay" +[[payu.wallet]] + payment_method_type = "google_pay" [payu.connector_auth.BodyKey] api_key="API Key" key1="Merchant POS ID" @@ -457,18 +1576,129 @@ merchant_name="Google Pay Merchant Name" gateway_merchant_id="Google Pay Merchant Key" merchant_id="Google Pay Merchant ID" -[prophetpay] -card_redirect = ["card_redirect"] -[prophetpay.connector_auth.SignatureKey] -api_key="Username" -key1="Token" -api_secret="Profile" +[placetopay] +[[placetopay.credit]] + payment_method_type = "Mastercard" +[[placetopay.credit]] + payment_method_type = "Visa" +[[placetopay.credit]] + payment_method_type = "Interac" +[[placetopay.credit]] + payment_method_type = "AmericanExpress" +[[placetopay.credit]] + payment_method_type = "JCB" +[[placetopay.credit]] + payment_method_type = "DinersClub" +[[placetopay.credit]] + payment_method_type = "Discover" +[[placetopay.credit]] + payment_method_type = "CartesBancaires" +[[placetopay.credit]] + payment_method_type = "UnionPay" +[[placetopay.debit]] + payment_method_type = "Mastercard" +[[placetopay.debit]] + payment_method_type = "Visa" +[[placetopay.debit]] + payment_method_type = "Interac" +[[placetopay.debit]] + payment_method_type = "AmericanExpress" +[[placetopay.debit]] + payment_method_type = "JCB" +[[placetopay.debit]] + payment_method_type = "DinersClub" +[[placetopay.debit]] + payment_method_type = "Discover" +[[placetopay.debit]] + payment_method_type = "CartesBancaires" +[[placetopay.debit]] + payment_method_type = "UnionPay" +[placetopay.connector_auth.BodyKey] +api_key="Login" +key1="Trankey" +[powertranz] +[[powertranz.credit]] + payment_method_type = "Mastercard" +[[powertranz.credit]] + payment_method_type = "Visa" +[[powertranz.credit]] + payment_method_type = "Interac" +[[powertranz.credit]] + payment_method_type = "AmericanExpress" +[[powertranz.credit]] + payment_method_type = "JCB" +[[powertranz.credit]] + payment_method_type = "DinersClub" +[[powertranz.credit]] + payment_method_type = "Discover" +[[powertranz.credit]] + payment_method_type = "CartesBancaires" +[[powertranz.credit]] + payment_method_type = "UnionPay" +[[powertranz.debit]] + payment_method_type = "Mastercard" +[[powertranz.debit]] + payment_method_type = "Visa" +[[powertranz.debit]] + payment_method_type = "Interac" +[[powertranz.debit]] + payment_method_type = "AmericanExpress" +[[powertranz.debit]] + payment_method_type = "JCB" +[[powertranz.debit]] + payment_method_type = "DinersClub" +[[powertranz.debit]] + payment_method_type = "Discover" +[[powertranz.debit]] + payment_method_type = "CartesBancaires" +[[powertranz.debit]] + payment_method_type = "UnionPay" +[powertranz.connector_auth.BodyKey] +key1 = "PowerTranz Id" +api_key="PowerTranz Password" +[powertranz.connector_webhook_details] +merchant_secret="Source verification key" [rapyd] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay"] +[[rapyd.credit]] + payment_method_type = "Mastercard" +[[rapyd.credit]] + payment_method_type = "Visa" +[[rapyd.credit]] + payment_method_type = "Interac" +[[rapyd.credit]] + payment_method_type = "AmericanExpress" +[[rapyd.credit]] + payment_method_type = "JCB" +[[rapyd.credit]] + payment_method_type = "DinersClub" +[[rapyd.credit]] + payment_method_type = "Discover" +[[rapyd.credit]] + payment_method_type = "CartesBancaires" +[[rapyd.credit]] + payment_method_type = "UnionPay" +[[rapyd.debit]] + payment_method_type = "Mastercard" +[[rapyd.debit]] + payment_method_type = "Visa" +[[rapyd.debit]] + payment_method_type = "Interac" +[[rapyd.debit]] + payment_method_type = "AmericanExpress" +[[rapyd.debit]] + payment_method_type = "JCB" +[[rapyd.debit]] + payment_method_type = "DinersClub" +[[rapyd.debit]] + payment_method_type = "Discover" +[[rapyd.debit]] + payment_method_type = "CartesBancaires" +[[rapyd.debit]] + payment_method_type = "UnionPay" +[[rapyd.wallet]] + payment_method_type = "apple_pay" [rapyd.connector_auth.BodyKey] api_key="Access Key" key1="API Secret" @@ -488,22 +1718,136 @@ merchant_capabilities=["supports3DS"] label="apple" [shift4] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps"] +[[shift4.credit]] + payment_method_type = "Mastercard" +[[shift4.credit]] + payment_method_type = "Visa" +[[shift4.credit]] + payment_method_type = "Interac" +[[shift4.credit]] + payment_method_type = "AmericanExpress" +[[shift4.credit]] + payment_method_type = "JCB" +[[shift4.credit]] + payment_method_type = "DinersClub" +[[shift4.credit]] + payment_method_type = "Discover" +[[shift4.credit]] + payment_method_type = "CartesBancaires" +[[shift4.credit]] + payment_method_type = "UnionPay" +[[shift4.debit]] + payment_method_type = "Mastercard" +[[shift4.debit]] + payment_method_type = "Visa" +[[shift4.debit]] + payment_method_type = "Interac" +[[shift4.debit]] + payment_method_type = "AmericanExpress" +[[shift4.debit]] + payment_method_type = "JCB" +[[shift4.debit]] + payment_method_type = "DinersClub" +[[shift4.debit]] + payment_method_type = "Discover" +[[shift4.debit]] + payment_method_type = "CartesBancaires" +[[shift4.debit]] + payment_method_type = "UnionPay" +[[shift4.bank_redirect]] + payment_method_type = "ideal" +[[shift4.bank_redirect]] + payment_method_type = "giropay" +[[shift4.bank_redirect]] + payment_method_type = "sofort" +[[shift4.bank_redirect]] + payment_method_type = "eps" [shift4.connector_auth.HeaderKey] api_key="API Key" [shift4.connector_webhook_details] merchant_secret="Source verification key" [stripe] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -pay_later=["klarna","affirm","afterpay_clearpay"] -bank_redirect=["ideal","giropay","sofort","eps","bancontact_card","przelewy24"] -bank_debit=["ach","bacs","becs","sepa"] -bank_transfer=["ach","bacs","sepa", "multibanco"] -wallet = ["apple_pay","google_pay","we_chat_pay","ali_pay", "cashapp"] +[[stripe.credit]] + payment_method_type = "Mastercard" +[[stripe.credit]] + payment_method_type = "Visa" +[[stripe.credit]] + payment_method_type = "Interac" +[[stripe.credit]] + payment_method_type = "AmericanExpress" +[[stripe.credit]] + payment_method_type = "JCB" +[[stripe.credit]] + payment_method_type = "DinersClub" +[[stripe.credit]] + payment_method_type = "Discover" +[[stripe.credit]] + payment_method_type = "CartesBancaires" +[[stripe.credit]] + payment_method_type = "UnionPay" +[[stripe.debit]] + payment_method_type = "Mastercard" +[[stripe.debit]] + payment_method_type = "Visa" +[[stripe.debit]] + payment_method_type = "Interac" +[[stripe.debit]] + payment_method_type = "AmericanExpress" +[[stripe.debit]] + payment_method_type = "JCB" +[[stripe.debit]] + payment_method_type = "DinersClub" +[[stripe.debit]] + payment_method_type = "Discover" +[[stripe.debit]] + payment_method_type = "CartesBancaires" +[[stripe.debit]] + payment_method_type = "UnionPay" +[[stripe.pay_later]] + payment_method_type = "klarna" +[[stripe.pay_later]] + payment_method_type = "affirm" +[[stripe.pay_later]] + payment_method_type = "afterpay_clearpay" +[[stripe.bank_redirect]] + payment_method_type = "ideal" +[[stripe.bank_redirect]] + payment_method_type = "giropay" +[[stripe.bank_redirect]] + payment_method_type = "sofort" +[[stripe.bank_redirect]] + payment_method_type = "eps" +[[stripe.bank_redirect]] + payment_method_type = "bancontact_card" +[[stripe.bank_redirect]] + payment_method_type = "przelewy24" +[[stripe.bank_debit]] + payment_method_type = "ach" +[[stripe.bank_debit]] + payment_method_type = "bacs" +[[stripe.bank_debit]] + payment_method_type = "becs" +[[stripe.bank_debit]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "ach" +[[stripe.bank_transfer]] + payment_method_type = "bacs" +[[stripe.bank_transfer]] + payment_method_type = "sepa" +[[stripe.bank_transfer]] + payment_method_type = "multibanco" +[[stripe.wallet]] + payment_method_type = "apple_pay" +[[stripe.wallet]] + payment_method_type = "google_pay" +[[stripe.wallet]] + payment_method_type = "we_chat_pay" +[[stripe.wallet]] + payment_method_type = "ali_pay" +[[stripe.wallet]] + payment_method_type = "cashapp" is_verifiable = true [stripe.connector_auth.HeaderKey] api_key="Secret Key" @@ -527,30 +1871,144 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" -[zen] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -voucher = ["boleto", "efecty", "pago_efectivo", "red_compra", "red_pagos"] -bank_transfer = ["pix", "pse"] -wallet = ["apple_pay","google_pay"] -[zen.connector_auth.HeaderKey] -api_key="API Key" -[zen.connector_webhook_details] +[stax] +[[stax.credit]] + payment_method_type = "Mastercard" +[[stax.credit]] + payment_method_type = "Visa" +[[stax.credit]] + payment_method_type = "Interac" +[[stax.credit]] + payment_method_type = "AmericanExpress" +[[stax.credit]] + payment_method_type = "JCB" +[[stax.credit]] + payment_method_type = "DinersClub" +[[stax.credit]] + payment_method_type = "Discover" +[[stax.credit]] + payment_method_type = "CartesBancaires" +[[stax.credit]] + payment_method_type = "UnionPay" +[[stax.debit]] + payment_method_type = "Mastercard" +[[stax.debit]] + payment_method_type = "Visa" +[[stax.debit]] + payment_method_type = "Interac" +[[stax.debit]] + payment_method_type = "AmericanExpress" +[[stax.debit]] + payment_method_type = "JCB" +[[stax.debit]] + payment_method_type = "DinersClub" +[[stax.debit]] + payment_method_type = "Discover" +[[stax.debit]] + payment_method_type = "CartesBancaires" +[[stax.debit]] + payment_method_type = "UnionPay" +[[stax.bank_debit]] + payment_method_type = "ach" +[stax.connector_auth.HeaderKey] +api_key="Api Key" +[stax.connector_webhook_details] merchant_secret="Source verification key" -[zen.metadata.apple_pay] -terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" -[zen.metadata.google_pay] -terminal_uuid="Terminal UUID" -pay_wall_secret="Pay Wall Secret" - +[square] +[[square.credit]] + payment_method_type = "Mastercard" +[[square.credit]] + payment_method_type = "Visa" +[[square.credit]] + payment_method_type = "Interac" +[[square.credit]] + payment_method_type = "AmericanExpress" +[[square.credit]] + payment_method_type = "JCB" +[[square.credit]] + payment_method_type = "DinersClub" +[[square.credit]] + payment_method_type = "Discover" +[[square.credit]] + payment_method_type = "CartesBancaires" +[[square.credit]] + payment_method_type = "UnionPay" +[[square.debit]] + payment_method_type = "Mastercard" +[[square.debit]] + payment_method_type = "Visa" +[[square.debit]] + payment_method_type = "Interac" +[[square.debit]] + payment_method_type = "AmericanExpress" +[[square.debit]] + payment_method_type = "JCB" +[[square.debit]] + payment_method_type = "DinersClub" +[[square.debit]] + payment_method_type = "Discover" +[[square.debit]] + payment_method_type = "CartesBancaires" +[[square.debit]] + payment_method_type = "UnionPay" +[square_payout.connector_auth.BodyKey] +api_key = "Square API Key" +key1 = "Square Client Id" +[square.connector_webhook_details] +merchant_secret="Source verification key" [trustpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay","sofort","eps","blik"] -wallet = ["apple_pay","google_pay"] +[[trustpay.credit]] + payment_method_type = "Mastercard" +[[trustpay.credit]] + payment_method_type = "Visa" +[[trustpay.credit]] + payment_method_type = "Interac" +[[trustpay.credit]] + payment_method_type = "AmericanExpress" +[[trustpay.credit]] + payment_method_type = "JCB" +[[trustpay.credit]] + payment_method_type = "DinersClub" +[[trustpay.credit]] + payment_method_type = "Discover" +[[trustpay.credit]] + payment_method_type = "CartesBancaires" +[[trustpay.credit]] + payment_method_type = "UnionPay" +[[trustpay.debit]] + payment_method_type = "Mastercard" +[[trustpay.debit]] + payment_method_type = "Visa" +[[trustpay.debit]] + payment_method_type = "Interac" +[[trustpay.debit]] + payment_method_type = "AmericanExpress" +[[trustpay.debit]] + payment_method_type = "JCB" +[[trustpay.debit]] + payment_method_type = "DinersClub" +[[trustpay.debit]] + payment_method_type = "Discover" +[[trustpay.debit]] + payment_method_type = "CartesBancaires" +[[trustpay.debit]] + payment_method_type = "UnionPay" +[[trustpay.bank_redirect]] + payment_method_type = "ideal" +[[trustpay.bank_redirect]] + payment_method_type = "giropay" +[[trustpay.bank_redirect]] + payment_method_type = "sofort" +[[trustpay.bank_redirect]] + payment_method_type = "eps" +[[trustpay.bank_redirect]] + payment_method_type = "blik" +[[trustpay.wallet]] + payment_method_type = "apple_pay" +[[trustpay.wallet]] + payment_method_type = "google_pay" [trustpay.connector_auth.SignatureKey] api_key="API Key" key1="Project ID" @@ -570,10 +2028,100 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" +[tsys] +[[tsys.credit]] + payment_method_type = "Mastercard" +[[tsys.credit]] + payment_method_type = "Visa" +[[tsys.credit]] + payment_method_type = "Interac" +[[tsys.credit]] + payment_method_type = "AmericanExpress" +[[tsys.credit]] + payment_method_type = "JCB" +[[tsys.credit]] + payment_method_type = "DinersClub" +[[tsys.credit]] + payment_method_type = "Discover" +[[tsys.credit]] + payment_method_type = "CartesBancaires" +[[tsys.credit]] + payment_method_type = "UnionPay" +[[tsys.debit]] + payment_method_type = "Mastercard" +[[tsys.debit]] + payment_method_type = "Visa" +[[tsys.debit]] + payment_method_type = "Interac" +[[tsys.debit]] + payment_method_type = "AmericanExpress" +[[tsys.debit]] + payment_method_type = "JCB" +[[tsys.debit]] + payment_method_type = "DinersClub" +[[tsys.debit]] + payment_method_type = "Discover" +[[tsys.debit]] + payment_method_type = "CartesBancaires" +[[tsys.debit]] + payment_method_type = "UnionPay" +[tsys.connector_auth.SignatureKey] +api_key="Device Id" +key1="Transaction Key" +api_secret="Developer Id" +[tsys.connector_webhook_details] +merchant_secret="Source verification key" + +[volt] +[[volt.bank_redirect]] + payment_method_type = "open_banking_uk" +[volt.connector_auth.MultiAuthKey] +api_key = "Username" +api_secret = "Password" +key1 = "Client ID" +key2 = "Client Secret" + [worldline] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_redirect=["ideal","giropay"] +[[worldline.credit]] + payment_method_type = "Mastercard" +[[worldline.credit]] + payment_method_type = "Visa" +[[worldline.credit]] + payment_method_type = "Interac" +[[worldline.credit]] + payment_method_type = "AmericanExpress" +[[worldline.credit]] + payment_method_type = "JCB" +[[worldline.credit]] + payment_method_type = "DinersClub" +[[worldline.credit]] + payment_method_type = "Discover" +[[worldline.credit]] + payment_method_type = "CartesBancaires" +[[worldline.credit]] + payment_method_type = "UnionPay" +[[worldline.debit]] + payment_method_type = "Mastercard" +[[worldline.debit]] + payment_method_type = "Visa" +[[worldline.debit]] + payment_method_type = "Interac" +[[worldline.debit]] + payment_method_type = "AmericanExpress" +[[worldline.debit]] + payment_method_type = "JCB" +[[worldline.debit]] + payment_method_type = "DinersClub" +[[worldline.debit]] + payment_method_type = "Discover" +[[worldline.debit]] + payment_method_type = "CartesBancaires" +[[worldline.debit]] + payment_method_type = "UnionPay" +[[worldline.bank_redirect]] + payment_method_type = "ideal" +[[worldline.bank_redirect]] + payment_method_type = "giropay" [worldline.connector_auth.SignatureKey] api_key="API Key ID" key1="Merchant ID" @@ -582,9 +2130,46 @@ api_secret="Secret API Key" merchant_secret="Source verification key" [worldpay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","apple_pay"] +[[worldpay.credit]] + payment_method_type = "Mastercard" +[[worldpay.credit]] + payment_method_type = "Visa" +[[worldpay.credit]] + payment_method_type = "Interac" +[[worldpay.credit]] + payment_method_type = "AmericanExpress" +[[worldpay.credit]] + payment_method_type = "JCB" +[[worldpay.credit]] + payment_method_type = "DinersClub" +[[worldpay.credit]] + payment_method_type = "Discover" +[[worldpay.credit]] + payment_method_type = "CartesBancaires" +[[worldpay.credit]] + payment_method_type = "UnionPay" +[[worldpay.debit]] + payment_method_type = "Mastercard" +[[worldpay.debit]] + payment_method_type = "Visa" +[[worldpay.debit]] + payment_method_type = "Interac" +[[worldpay.debit]] + payment_method_type = "AmericanExpress" +[[worldpay.debit]] + payment_method_type = "JCB" +[[worldpay.debit]] + payment_method_type = "DinersClub" +[[worldpay.debit]] + payment_method_type = "Discover" +[[worldpay.debit]] + payment_method_type = "CartesBancaires" +[[worldpay.debit]] + payment_method_type = "UnionPay" +[[worldpay.wallet]] + payment_method_type = "google_pay" +[[worldpay.wallet]] + payment_method_type = "apple_pay" [worldpay.connector_auth.BodyKey] api_key="Username" key1="Password" @@ -608,179 +2193,308 @@ supported_networks=["visa","masterCard","amex","discover"] merchant_capabilities=["supports3DS"] label="apple" -[cashtocode] -reward = ["classic", "evoucher"] -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.EUR] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.GBP] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.USD] -password_classic="Password Classic" -username_classic="Username Classic" -merchant_id_classic="MerchantId Classic" -password_evoucher="Password Evoucher" -username_evoucher="Username Evoucher" -merchant_id_evoucher="MerchantId Evoucher" -[cashtocode.connector_webhook_details] -merchant_secret="Source verification key" - -[cryptopay] -crypto = ["crypto_currency"] -[cryptopay.connector_auth.BodyKey] +[zen] +[[zen.credit]] + payment_method_type = "Mastercard" +[[zen.credit]] + payment_method_type = "Visa" +[[zen.credit]] + payment_method_type = "Interac" +[[zen.credit]] + payment_method_type = "AmericanExpress" +[[zen.credit]] + payment_method_type = "JCB" +[[zen.credit]] + payment_method_type = "DinersClub" +[[zen.credit]] + payment_method_type = "Discover" +[[zen.credit]] + payment_method_type = "CartesBancaires" +[[zen.credit]] + payment_method_type = "UnionPay" +[[zen.debit]] + payment_method_type = "Mastercard" +[[zen.debit]] + payment_method_type = "Visa" +[[zen.debit]] + payment_method_type = "Interac" +[[zen.debit]] + payment_method_type = "AmericanExpress" +[[zen.debit]] + payment_method_type = "JCB" +[[zen.debit]] + payment_method_type = "DinersClub" +[[zen.debit]] + payment_method_type = "Discover" +[[zen.debit]] + payment_method_type = "CartesBancaires" +[[zen.debit]] + payment_method_type = "UnionPay" +[[zen.voucher]] + payment_method_type = "boleto" +[[zen.voucher]] + payment_method_type = "efecty" +[[zen.voucher]] + payment_method_type = "pago_efectivo" +[[zen.voucher]] + payment_method_type = "red_compra" +[[zen.voucher]] + payment_method_type = "red_pagos" +[[zen.bank_transfer]] + payment_method_type = "pix" +[[zen.bank_transfer]] + payment_method_type = "pse" +[[zen.wallet]] + payment_method_type = "apple_pay" +[[zen.wallet]] + payment_method_type = "google_pay" +[zen.connector_auth.HeaderKey] api_key="API Key" -key1="Secret Key" -[cryptopay.connector_webhook_details] +[zen.connector_webhook_details] merchant_secret="Source verification key" +[zen.metadata.apple_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" +[zen.metadata.google_pay] +terminal_uuid="Terminal UUID" +pay_wall_secret="Pay Wall Secret" + + [dummy_connector] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[dummy_connector.credit]] + payment_method_type = "Mastercard" +[[dummy_connector.credit]] + payment_method_type = "Visa" +[[dummy_connector.credit]] + payment_method_type = "Interac" +[[dummy_connector.credit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.credit]] + payment_method_type = "JCB" +[[dummy_connector.credit]] + payment_method_type = "DinersClub" +[[dummy_connector.credit]] + payment_method_type = "Discover" +[[dummy_connector.credit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.credit]] + payment_method_type = "UnionPay" +[[dummy_connector.debit]] + payment_method_type = "Mastercard" +[[dummy_connector.debit]] + payment_method_type = "Visa" +[[dummy_connector.debit]] + payment_method_type = "Interac" +[[dummy_connector.debit]] + payment_method_type = "AmericanExpress" +[[dummy_connector.debit]] + payment_method_type = "JCB" +[[dummy_connector.debit]] + payment_method_type = "DinersClub" +[[dummy_connector.debit]] + payment_method_type = "Discover" +[[dummy_connector.debit]] + payment_method_type = "CartesBancaires" +[[dummy_connector.debit]] + payment_method_type = "UnionPay" [dummy_connector.connector_auth.HeaderKey] api_key="Api Key" -[stripe_test] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["google_pay","ali_pay"] -pay_later=["klarna","affirm","afterpay_clearpay"] -[stripe_test.connector_auth.HeaderKey] -api_key="Api Key" - [paypal_test] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["paypal"] +[[paypal_test.credit]] + payment_method_type = "Mastercard" +[[paypal_test.credit]] + payment_method_type = "Visa" +[[paypal_test.credit]] + payment_method_type = "Interac" +[[paypal_test.credit]] + payment_method_type = "AmericanExpress" +[[paypal_test.credit]] + payment_method_type = "JCB" +[[paypal_test.credit]] + payment_method_type = "DinersClub" +[[paypal_test.credit]] + payment_method_type = "Discover" +[[paypal_test.credit]] + payment_method_type = "CartesBancaires" +[[paypal_test.credit]] + payment_method_type = "UnionPay" +[[paypal_test.debit]] + payment_method_type = "Mastercard" +[[paypal_test.debit]] + payment_method_type = "Visa" +[[paypal_test.debit]] + payment_method_type = "Interac" +[[paypal_test.debit]] + payment_method_type = "AmericanExpress" +[[paypal_test.debit]] + payment_method_type = "JCB" +[[paypal_test.debit]] + payment_method_type = "DinersClub" +[[paypal_test.debit]] + payment_method_type = "Discover" +[[paypal_test.debit]] + payment_method_type = "CartesBancaires" +[[paypal_test.debit]] + payment_method_type = "UnionPay" +[[paypal_test.wallet]] + payment_method_type = "paypal" [paypal_test.connector_auth.HeaderKey] api_key="Api Key" -[payme] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[payme.connector_auth.BodyKey] -api_key="Seller Payme Id" -key1="Payme Public Key" -[payme.connector_webhook_details] -merchant_secret="Payme Client Secret" -additional_secret="Payme Client Key" - -[powertranz] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[powertranz.connector_auth.BodyKey] -key1 = "PowerTranz Id" -api_key="PowerTranz Password" -[powertranz.connector_webhook_details] -merchant_secret="Source verification key" - -[globepay] -wallet = ["we_chat_pay","ali_pay"] -[globepay.connector_auth.BodyKey] -api_key="Partner Code" -key1="Credential Code" -[globepay.connector_webhook_details] -merchant_secret="Source verification key" - -[tsys] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[tsys.connector_auth.SignatureKey] -api_key="Device Id" -key1="Transaction Key" -api_secret="Developer Id" -[tsys.connector_webhook_details] -merchant_secret="Source verification key" - -[square] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -[square_payout.connector_auth.BodyKey] -api_key = "Square API Key" -key1 = "Square Client Id" -[square.connector_webhook_details] -merchant_secret="Source verification key" +[stripe_test] +[[stripe_test.credit]] + payment_method_type = "Mastercard" +[[stripe_test.credit]] + payment_method_type = "Visa" +[[stripe_test.credit]] + payment_method_type = "Interac" +[[stripe_test.credit]] + payment_method_type = "AmericanExpress" +[[stripe_test.credit]] + payment_method_type = "JCB" +[[stripe_test.credit]] + payment_method_type = "DinersClub" +[[stripe_test.credit]] + payment_method_type = "Discover" +[[stripe_test.credit]] + payment_method_type = "CartesBancaires" +[[stripe_test.credit]] + payment_method_type = "UnionPay" +[[stripe_test.debit]] + payment_method_type = "Mastercard" +[[stripe_test.debit]] + payment_method_type = "Visa" +[[stripe_test.debit]] + payment_method_type = "Interac" +[[stripe_test.debit]] + payment_method_type = "AmericanExpress" +[[stripe_test.debit]] + payment_method_type = "JCB" +[[stripe_test.debit]] + payment_method_type = "DinersClub" +[[stripe_test.debit]] + payment_method_type = "Discover" +[[stripe_test.debit]] + payment_method_type = "CartesBancaires" +[[stripe_test.debit]] + payment_method_type = "UnionPay" +[[stripe_test.wallet]] + payment_method_type = "google_pay" +[[stripe_test.wallet]] + payment_method_type = "ali_pay" +[[stripe_test.wallet]] + payment_method_type = "we_chat_pay" +[[stripe_test.pay_later]] + payment_method_type = "klarna" +[[stripe_test.pay_later]] + payment_method_type = "affirm" +[[stripe_test.pay_later]] + payment_method_type = "afterpay_clearpay" +[[paypal_test.wallet]] + payment_method_type = "paypal" +[stripe_test.connector_auth.HeaderKey] +api_key="Api Key" -[stax] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -bank_debit=["ach"] -[stax.connector_auth.HeaderKey] +[helcim] +[[helcim.credit]] + payment_method_type = "Mastercard" +[[helcim.credit]] + payment_method_type = "Visa" +[[helcim.credit]] + payment_method_type = "Interac" +[[helcim.credit]] + payment_method_type = "AmericanExpress" +[[helcim.credit]] + payment_method_type = "JCB" +[[helcim.credit]] + payment_method_type = "DinersClub" +[[helcim.credit]] + payment_method_type = "Discover" +[[helcim.credit]] + payment_method_type = "CartesBancaires" +[[helcim.credit]] + payment_method_type = "UnionPay" +[[helcim.debit]] + payment_method_type = "Mastercard" +[[helcim.debit]] + payment_method_type = "Visa" +[[helcim.debit]] + payment_method_type = "Interac" +[[helcim.debit]] + payment_method_type = "AmericanExpress" +[[helcim.debit]] + payment_method_type = "JCB" +[[helcim.debit]] + payment_method_type = "DinersClub" +[[helcim.debit]] + payment_method_type = "Discover" +[[helcim.debit]] + payment_method_type = "CartesBancaires" +[[helcim.debit]] + payment_method_type = "UnionPay" +[helcim.connector_auth.HeaderKey] api_key="Api Key" -[stax.connector_webhook_details] -merchant_secret="Source verification key" -[volt] -bank_redirect = ["open_banking_uk"] -[volt.connector_auth.MultiAuthKey] -api_key = "Username" -api_secret = "Password" -key1 = "Client ID" -key2 = "Client Secret" -[wise_payout] -bank_transfer = ["ach","bacs","sepa"] -[wise_payout.connector_auth.BodyKey] -api_key = "Wise API Key" -key1 = "Wise Account Id" + [adyen_payout] -bank_transfer = ["ach","bacs","sepa"] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] +[[adyen_payout.credit]] + payment_method_type = "Mastercard" +[[adyen_payout.credit]] + payment_method_type = "Visa" +[[adyen_payout.credit]] + payment_method_type = "Interac" +[[adyen_payout.credit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.credit]] + payment_method_type = "JCB" +[[adyen_payout.credit]] + payment_method_type = "DinersClub" +[[adyen_payout.credit]] + payment_method_type = "Discover" +[[adyen_payout.credit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.credit]] + payment_method_type = "UnionPay" +[[adyen_payout.debit]] + payment_method_type = "Mastercard" +[[adyen_payout.debit]] + payment_method_type = "Visa" +[[adyen_payout.debit]] + payment_method_type = "Interac" +[[adyen_payout.debit]] + payment_method_type = "AmericanExpress" +[[adyen_payout.debit]] + payment_method_type = "JCB" +[[adyen_payout.debit]] + payment_method_type = "DinersClub" +[[adyen_payout.debit]] + payment_method_type = "Discover" +[[adyen_payout.debit]] + payment_method_type = "CartesBancaires" +[[adyen_payout.debit]] + payment_method_type = "UnionPay" +[[adyen_payout.bank_transfer]] + payment_method_type = "ach" +[[adyen_payout.bank_transfer]] + payment_method_type = "bacs" +[[adyen_payout.bank_transfer]] + payment_method_type = "sepa" [adyen_payout.connector_auth.SignatureKey] api_key = "Adyen API Key (Payout creation)" api_secret = "Adyen Key (Payout submission)" key1 = "Adyen Account Id" -[gocardless] -bank_debit=["ach","becs","sepa"] -[gocardless.connector_auth.HeaderKey] -api_key="Access Token" -[gocardless.connector_webhook_details] -merchant_secret="Source verification key" - -[bankofamerica] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -wallet = ["apple_pay","google_pay"] - -[bankofamerica.connector_auth.SignatureKey] -api_key="Key" -key1="Merchant ID" -api_secret="Shared Secret" -[bankofamerica.connector_webhook_details] -merchant_secret="Source verification key" - -[bankofamerica.metadata.apple_pay.session_token_data] -certificate="Merchant Certificate (Base64 Encoded)" -certificate_keys="Merchant PrivateKey (Base64 Encoded)" -merchant_identifier="Apple Merchant Identifier" -display_name="Display Name" -initiative="Domain" -initiative_context="Domain Name" -[bankofamerica.metadata.apple_pay.payment_request_data] -supported_networks=["visa","masterCard","amex","discover"] -merchant_capabilities=["supports3DS"] -label="apple" - -[bankofamerica.metadata.google_pay] -merchant_name="Google Pay Merchant Name" -gateway_merchant_id="Google Pay Merchant Key" -merchant_id="Google Pay Merchant ID" - -[placetopay] -credit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] -debit = ["Mastercard","Visa","Interac","AmericanExpress","JCB","DinersClub","Discover","CartesBancaires","UnionPay"] - -[placetopay.connector_auth.BodyKey] -api_key="Login" -key1="Trankey" \ No newline at end of file +[wise_payout] +[[wise_payout.bank_transfer]] + payment_method_type = "ach" +[[wise_payout.bank_transfer]] + payment_method_type = "bacs" +[[wise_payout.bank_transfer]] + payment_method_type = "sepa" +[wise_payout.connector_auth.BodyKey] +api_key = "Wise API Key" +key1 = "Wise Account Id" \ No newline at end of file From 01b4ac30e40a55b05fe3585d0544b21125762bc7 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Mon, 8 Jan 2024 12:16:54 +0530 Subject: [PATCH 280/443] fix(connector): [Stripe] Deserialization Error while parsing Dispute Webhook Body (#3256) Co-authored-by: Arjun Karthik --- crates/router/src/connector/stripe.rs | 41 ++++++++++++++----- .../src/connector/stripe/transformers.rs | 14 +++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 65fd652629fb..8c43e2c16a25 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1858,10 +1858,15 @@ impl api::IncomingWebhook for Stripe { Ok(match details.event_data.event_object.object { stripe::WebhookEventObjectType::PaymentIntent => { - match details.event_data.event_object.metadata { + match details + .event_data + .event_object + .metadata + .and_then(|meta_data| meta_data.order_id) + { // if order_id is present - Some(meta_data) => api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::PaymentAttemptId(meta_data.order_id), + Some(order_id) => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId(order_id), ), // else used connector_transaction_id None => api_models::webhooks::ObjectReferenceId::PaymentId( @@ -1872,10 +1877,15 @@ impl api::IncomingWebhook for Stripe { } } stripe::WebhookEventObjectType::Charge => { - match details.event_data.event_object.metadata { + match details + .event_data + .event_object + .metadata + .and_then(|meta_data| meta_data.order_id) + { // if order_id is present - Some(meta_data) => api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::PaymentAttemptId(meta_data.order_id), + Some(order_id) => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId(order_id), ), // else used connector_transaction_id None => api_models::webhooks::ObjectReferenceId::PaymentId( @@ -1908,14 +1918,25 @@ impl api::IncomingWebhook for Stripe { ) } stripe::WebhookEventObjectType::Refund => { - match details.event_data.event_object.metadata { + match details + .event_data + .event_object + .metadata + .clone() + .and_then(|meta_data| meta_data.order_id) + { // if meta_data is present - Some(meta_data) => { + Some(order_id) => { // Issue: 2076 - match meta_data.is_refund_id_as_reference { + match details + .event_data + .event_object + .metadata + .and_then(|meta_data| meta_data.is_refund_id_as_reference) + { // if the order_id is refund_id Some(_) => api_models::webhooks::ObjectReferenceId::RefundId( - api_models::webhooks::RefundIdType::RefundId(meta_data.order_id), + api_models::webhooks::RefundIdType::RefundId(order_id), ), // if the order_id is payment_id // since payment_id was being passed before the deployment of this pr diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index ba5dfc7fef91..4338e8f9ff28 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -135,7 +135,7 @@ pub struct PaymentIntentRequest { pub struct StripeMetadata { // merchant_reference_id #[serde(rename(serialize = "metadata[order_id]"))] - pub order_id: String, + pub order_id: Option, // to check whether the order_id is refund_id or payemnt_id // before deployment, order id is set to payemnt_id in refunds but now it is set as refund_id // it is set as string instead of bool because stripe pass it as string even if we set it as bool @@ -1861,7 +1861,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { statement_descriptor_suffix: item.request.statement_descriptor_suffix.clone(), statement_descriptor: item.request.statement_descriptor.clone(), meta_data: StripeMetadata { - order_id, + order_id: Some(order_id), is_refund_id_as_reference: None, }, return_url: item @@ -2696,7 +2696,7 @@ impl TryFrom<&types::RefundsRouterData> for RefundRequest { amount: Some(amount), payment_intent, meta_data: StripeMetadata { - order_id: item.request.refund_id.clone(), + order_id: Some(item.request.refund_id.clone()), is_refund_id_as_reference: Some("true".to_string()), }, }) @@ -3203,7 +3203,7 @@ pub struct WebhookPaymentMethodDetails { pub payment_method: WebhookPaymentMethodType, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct WebhookEventObjectData { pub id: String, pub object: WebhookEventObjectType, @@ -3218,7 +3218,7 @@ pub struct WebhookEventObjectData { pub metadata: Option, } -#[derive(Debug, Deserialize, strum::Display)] +#[derive(Debug, Clone, Deserialize, strum::Display)] #[serde(rename_all = "snake_case")] pub enum WebhookEventObjectType { PaymentIntent, @@ -3280,7 +3280,7 @@ pub enum WebhookEventType { Unknown, } -#[derive(Debug, Serialize, strum::Display, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, strum::Display, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum WebhookEventStatus { WarningNeedsResponse, @@ -3304,7 +3304,7 @@ pub enum WebhookEventStatus { Unknown, } -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct EvidenceDetails { #[serde(with = "common_utils::custom_serde::timestamp")] pub due_by: PrimitiveDateTime, From 962592ab48ac6724d67882791033cb542dfcab0f Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:08:50 +0530 Subject: [PATCH 281/443] ci: add reusable workflow to create nightly tags in CalVer format (#3247) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .github/git-cliff-changelog.toml | 7 +- .github/workflows/release-new-version.yml | 95 -------- .../release-nightly-version-reusable.yml | 202 ++++++++++++++++++ .github/workflows/release-nightly-version.yml | 151 +++++++++++++ 4 files changed, 357 insertions(+), 98 deletions(-) delete mode 100644 .github/workflows/release-new-version.yml create mode 100644 .github/workflows/release-nightly-version-reusable.yml create mode 100644 .github/workflows/release-nightly-version.yml diff --git a/.github/git-cliff-changelog.toml b/.github/git-cliff-changelog.toml index 1d7e4080cc20..f9959eebfc69 100644 --- a/.github/git-cliff-changelog.toml +++ b/.github/git-cliff-changelog.toml @@ -14,7 +14,7 @@ body = """ {% set commit_base_url = "https://github.com/juspay/hyperswitch/commit/" -%} {% set compare_base_url = "https://github.com/juspay/hyperswitch/compare/" -%} {% if version -%} - ## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }}) + ## {{ version }} {% else -%} ## [unreleased] {% endif -%} @@ -69,7 +69,8 @@ commit_parsers = [ { message = "^(?i)(refactor)", group = "Refactors" }, { message = "^(?i)(test)", group = "Testing" }, { message = "^(?i)(docs)", group = "Documentation" }, - { message = "^(?i)(chore\\(version\\)): V[\\d]+\\.[\\d]+\\.[\\d]+", skip = true }, + { message = "^(?i)(chore\\(version\\)): (V|v)[\\d]+\\.[\\d]+\\.[\\d]+", skip = true }, + { message = "^(?i)(chore\\(version\\)): [0-9]{4}\\.[0-9]{2}\\.[0-9]{2}(\\.[0-9]+)?(-.+)?", skip = true }, { message = "^(?i)(chore)", group = "Miscellaneous Tasks" }, { message = "^(?i)(build)", group = "Build System / Dependencies" }, { message = "^(?i)(ci)", skip = true }, @@ -79,7 +80,7 @@ protect_breaking_commits = false # filter out the commits that are not matched by commit parsers filter_commits = false # glob pattern for matching git tags -tag_pattern = "v[0-9]*" +tag_pattern = "[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}(\\.[0-9]+)?(-.+)?" # regex for skipping tags # skip_tags = "v0.1.0-beta.1" # regex for ignoring tags diff --git a/.github/workflows/release-new-version.yml b/.github/workflows/release-new-version.yml deleted file mode 100644 index 2f8ae7e4819f..000000000000 --- a/.github/workflows/release-new-version.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Release a new version - -on: - schedule: - - cron: "30 14 * * 0-4" # Run workflow at 8 PM IST every Sunday-Thursday - - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # Allow more retries for network requests in cargo (downloading crates) and - # rustup (installing toolchains). This should help to reduce flaky CI failures - # from transient network timeouts or other issues. - CARGO_NET_RETRY: 10 - RUSTUP_MAX_RETRIES: 10 - -jobs: - create-release: - name: Release a new version - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.AUTO_RELEASE_PAT }} - - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable 2 weeks ago - - - name: Install cocogitto - uses: baptiste0928/cargo-install@v2.2.0 - with: - crate: cocogitto - version: 5.4.0 - - - name: Set Git Configuration - shell: bash - run: | - git config --local user.name 'github-actions' - git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' - - - name: Update Postman collection files from Postman directories - shell: bash - run: | - # maybe we need to move this package.json as we need it in multiple workflows - npm ci - POSTMAN_DIR=postman/collection-dir - POSTMAN_JSON_DIR=postman/collection-json - NEWMAN_PATH=$(pwd)/node_modules/.bin - export PATH=${NEWMAN_PATH}:${PATH} - # generate postman collections for all postman directories - for connector_dir in ${POSTMAN_DIR}/* - do - connector=$(basename ${connector_dir}) - newman dir-import ${POSTMAN_DIR}/${connector} -o ${POSTMAN_JSON_DIR}/${connector}.postman_collection.json - done - - if git add postman && ! git diff --staged --quiet postman; then - git commit --message 'test(postman): update postman collection files' - echo "Changes detected and commited." - fi - - - name: Obtain previous and new tag information - shell: bash - # Only consider tags on current branch when setting PREVIOUS_TAG - run: | - PREVIOUS_TAG="$(git tag --sort='version:refname' --merged | tail --lines 1)" - if [[ "$(cog bump --auto --dry-run)" == *"No conventional commits for your repository that required a bump"* ]]; then - NEW_TAG="$(cog bump --patch --dry-run)" - else - NEW_TAG="$(cog bump --auto --dry-run)" - fi - echo "NEW_TAG=${NEW_TAG}" >> $GITHUB_ENV - echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV - - - name: Update changelog and create tag - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - # Remove prefix 'v' from 'NEW_TAG' as cog bump --version expects only the version number - run: | - cog bump --version ${NEW_TAG#v} - - - name: Push created commit and tag - shell: bash - if: ${{ env.NEW_TAG != env.PREVIOUS_TAG }} - run: | - git push - git push --tags diff --git a/.github/workflows/release-nightly-version-reusable.yml b/.github/workflows/release-nightly-version-reusable.yml new file mode 100644 index 000000000000..deb8c44cc3c3 --- /dev/null +++ b/.github/workflows/release-nightly-version-reusable.yml @@ -0,0 +1,202 @@ +name: Create a nightly tag + +on: + workflow_call: + secrets: + app_id: + description: App ID for the GitHub app + required: true + app_private_key: + description: Private key for the GitHub app + required: true + outputs: + tag: + description: The tag that was created by the workflow + value: ${{ jobs.create-nightly-tag.outputs.tag }} + +env: + # Allow more retries for network requests in cargo (downloading crates) and + # rustup (installing toolchains). This should help to reduce flaky CI failures + # from transient network timeouts or other issues. + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + + # The branch name that this workflow is allowed to run on. + # If the workflow is run on any other branch, this workflow will fail. + ALLOWED_BRANCH_NAME: main + +jobs: + create-nightly-tag: + name: Create a nightly tag + runs-on: ubuntu-latest + + steps: + - name: Generate GitHub app token + id: generate_app_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.app_id }} + private-key: ${{ secrets.app_private_key }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if the workflow is run on an allowed branch + shell: bash + run: | + if [[ "${{github.ref}}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then + echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{github.ref}}'" + exit 1 + fi + + - name: Check if the latest commit is a tag + shell: bash + run: | + if [[ -n "$(git tag --points-at HEAD)" ]]; then + echo "::error::The latest commit on the branch is already a tag" + exit 1 + fi + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Install git-cliff + uses: baptiste0928/cargo-install@v2.2.0 + with: + crate: git-cliff + version: 1.4.0 + + - name: Obtain previous and next tag information + shell: bash + run: | + # Calendar versioning format followed: `YYYY.0M.0D.MICRO` + # - MICRO version number starts from 0 (to allow for multiple tags in a single day) + # - Hotfixes or patches can be suffixed as `-hotfix1` or `-patch1` after the MICRO version number + + CURRENT_UTC_DATE="$(date --utc '+%04Y.%02m.%02d')" + + # Check if any tags exist on the current branch which contain the current UTC date + if ! git tag --merged | grep --quiet "${CURRENT_UTC_DATE}"; then + # Search for date-like tags (no strict checking), sort and obtain previous tag + PREVIOUS_TAG="$( + git tag --merged \ + | grep --extended-regexp '[0-9]{4}\.[0-9]{2}\.[0-9]{2}' \ + | sort --version-sort \ + | tail --lines 1 + )" + + # No tags with current date exist, next tag will be just tagged with current date and micro version number 0 + NEXT_MICRO_VERSION_NUMBER='0' + NEXT_TAG="${CURRENT_UTC_DATE}.${NEXT_MICRO_VERSION_NUMBER}" + + else + # Some tags exist with current date, find out latest micro version number + PREVIOUS_TAG="$( + git tag --merged \ + | grep "${CURRENT_UTC_DATE}" \ + | sort --version-sort \ + | tail --lines 1 + )" + PREVIOUS_MICRO_VERSION_NUMBER="$( + echo -n "${PREVIOUS_TAG}" \ + | sed --regexp-extended 's/[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.([0-9]+))?(-(.+))?/\2/g' + )" + # ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^ ^^^^ + # YEAR MONTH DAY MICRO Any suffix, say `hotfix1` + # + # The 2nd capture group contains the micro version number + + if [[ -z "${PREVIOUS_MICRO_VERSION_NUMBER}" ]]; then + # Micro version number is empty, set next micro version as 1 + NEXT_MICRO_VERSION_NUMBER='1' + else + # Increment previous micro version by 1 and set it as next micro version + NEXT_MICRO_VERSION_NUMBER="$((PREVIOUS_MICRO_VERSION_NUMBER + 1))" + fi + + NEXT_TAG="${CURRENT_UTC_DATE}.${NEXT_MICRO_VERSION_NUMBER}" + fi + + echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV + echo "NEXT_TAG=${NEXT_TAG}" >> $GITHUB_ENV + + - name: Generate changelog + shell: bash + run: | + # Generate changelog content and store it in `release-notes.md` + git-cliff --config '.github/git-cliff-changelog.toml' --strip header --tag "${NEXT_TAG}" "${PREVIOUS_TAG}^.." \ + | sed "/## ${PREVIOUS_TAG}\$/,\$d" \ + | sed '$s/$/\n- - -/' > release-notes.md + + # Append release notes after the specified pattern in `CHANGELOG.md` + sed --in-place '0,/^- - -/!b; /^- - -/{ + a + r release-notes.md + }' CHANGELOG.md + rm release-notes.md + + # We make use of GitHub API calls to commit and tag the changelog instead of the simpler + # `git commit`, `git tag` and `git push` commands to have signed commits and tags + - name: Commit generated changelog and create tag + shell: bash + env: + GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} + run: | + HEAD_COMMIT="$(git rev-parse 'HEAD^{commit}')" + + # Create a tree based on the HEAD commit of the current branch and updated changelog file + TREE_SHA="$( + gh api \ + --method POST \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + '/repos/{owner}/{repo}/git/trees' \ + --raw-field base_tree="${HEAD_COMMIT}" \ + --raw-field 'tree[][path]=CHANGELOG.md' \ + --raw-field 'tree[][mode]=100644' \ + --raw-field 'tree[][type]=blob' \ + --field 'tree[][content]=@CHANGELOG.md' \ + --jq '.sha' + )" + + # Create a commit to point to the above created tree + NEW_COMMIT_SHA="$( + gh api \ + --method POST \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + '/repos/{owner}/{repo}/git/commits' \ + --raw-field "message=chore(version): ${NEXT_TAG}" \ + --raw-field "parents[]=${HEAD_COMMIT}" \ + --raw-field "tree=${TREE_SHA}" \ + --jq '.sha' + )" + + # Update the current branch to point to the above created commit + # We disable forced update so that the workflow will fail if the branch has been updated since the workflow started + # (for example, new commits were pushed to the branch after the workflow execution started). + gh api \ + --method PATCH \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/{owner}/{repo}/git/refs/heads/${ALLOWED_BRANCH_NAME}" \ + --raw-field "sha=${NEW_COMMIT_SHA}" \ + --field 'force=false' + + # Create a lightweight tag to point to the above created commit + gh api \ + --method POST \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + '/repos/{owner}/{repo}/git/refs' \ + --raw-field "ref=refs/tags/${NEXT_TAG}" \ + --raw-field "sha=${NEW_COMMIT_SHA}" + + - name: Set job outputs + shell: bash + run: | + echo "tag=${NEXT_TAG}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/release-nightly-version.yml b/.github/workflows/release-nightly-version.yml new file mode 100644 index 000000000000..36a843469d0c --- /dev/null +++ b/.github/workflows/release-nightly-version.yml @@ -0,0 +1,151 @@ +name: Create a nightly tag + +on: + schedule: + - cron: "0 0 * * 1-5" # Run workflow at 00:00 midnight UTC (05:30 AM IST) every Monday-Friday + + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Allow more retries for network requests in cargo (downloading crates) and + # rustup (installing toolchains). This should help to reduce flaky CI failures + # from transient network timeouts or other issues. + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + + # The branch name that this workflow is allowed to run on. + # If the workflow is run on any other branch, this workflow will fail. + ALLOWED_BRANCH_NAME: main + +jobs: + update-postman-collections: + name: Update Postman collection JSON files + runs-on: ubuntu-latest + + steps: + - name: Generate GitHub app token + id: generate_app_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if the workflow is run on an allowed branch + shell: bash + run: | + if [[ "${{github.ref}}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then + echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{github.ref}}'" + exit 1 + fi + + - name: Check if the latest commit is a tag + shell: bash + run: | + if [[ -n "$(git tag --points-at HEAD)" ]]; then + echo "::error::The latest commit on the branch is already a tag" + exit 1 + fi + + - name: Update Postman collection files from Postman directories + shell: bash + run: | + # maybe we need to move this package.json as we need it in multiple workflows + npm ci + + POSTMAN_DIR="postman/collection-dir" + POSTMAN_JSON_DIR="postman/collection-json" + NEWMAN_PATH="$(pwd)/node_modules/.bin" + export PATH="${NEWMAN_PATH}:${PATH}" + + # generate Postman collection JSON files for all Postman collection directories + for connector_dir in "${POSTMAN_DIR}"/* + do + connector="$(basename "${connector_dir}")" + newman dir-import "${POSTMAN_DIR}/${connector}" -o "${POSTMAN_JSON_DIR}/${connector}.postman_collection.json" + done + + if git add postman && ! git diff --staged --quiet postman; then + echo "POSTMAN_COLLECTION_FILES_UPDATED=true" >> $GITHUB_ENV + echo "Postman collection files have been modified" + else + echo "Postman collection files have no modifications" + fi + + - name: Commit updated Postman collections if modified + shell: bash + env: + GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} + if: ${{ env.POSTMAN_COLLECTION_FILES_UPDATED == 'true' }} + run: | + # Obtain current HEAD commit SHA and use that as base tree SHA for creating a new tree + HEAD_COMMIT="$(git rev-parse 'HEAD^{commit}')" + UPDATED_TREE_SHA="${HEAD_COMMIT}" + + # Obtain the flags to be passed to the GitHub CLI. + # Each line contains the flags to be used corresponding to the file. + lines="$( + git ls-files \ + --format '--raw-field tree[][path]=%(path) --raw-field tree[][mode]=%(objectmode) --raw-field tree[][type]=%(objecttype) --field tree[][content]=@%(path)' \ + postman/collection-json + )" + + # Create a tree based on the HEAD commit of the current branch, using the contents of the updated Postman collections directory + while IFS= read -r line; do + # Split each line by space to obtain the flags passed to the GitHub CLI as an array + IFS=' ' read -ra flags <<< "${line}" + + # Create a tree by updating each collection JSON file. + # The SHA of the created tree is used as the base tree SHA for updating the next collection file. + UPDATED_TREE_SHA="$( + gh api \ + --method POST \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + '/repos/{owner}/{repo}/git/trees' \ + --raw-field base_tree="${UPDATED_TREE_SHA}" \ + "${flags[@]}" \ + --jq '.sha' + )" + done <<< "${lines}" + + # Create a commit to point to the tree with all updated collections + NEW_COMMIT_SHA="$( + gh api \ + --method POST \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + '/repos/{owner}/{repo}/git/commits' \ + --raw-field "message=chore(postman): update Postman collection files" \ + --raw-field "parents[]=${HEAD_COMMIT}" \ + --raw-field "tree=${UPDATED_TREE_SHA}" \ + --jq '.sha' + )" + + # Update the current branch to point to the above created commit. + # We disable forced update so that the workflow will fail if the branch has been updated since the workflow started + # (for example, new commits were pushed to the branch after the workflow execution started). + gh api \ + --method PATCH \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/{owner}/{repo}/git/refs/heads/${ALLOWED_BRANCH_NAME}" \ + --raw-field "sha=${NEW_COMMIT_SHA}" \ + --field 'force=false' + + create-nightly-tag: + name: Create a nightly tag + uses: ./.github/workflows/release-nightly-version-reusable.yml + needs: + - update-postman-collections + secrets: + app_id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + app_private_key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} From 7766245478f72b0bc942922b1138c87a239be153 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:09:18 +0530 Subject: [PATCH 282/443] feat(payments): add payment id in all the payment logs (#3142) --- crates/router/src/routes/payments.rs | 69 ++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index b836f02cded2..2ae1d620ccdd 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -94,7 +94,7 @@ use crate::{ operation_id = "Create a Payment", security(("api_key" = [])), )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreate, payment_id))] pub async fn payments_create( state: web::Data, req: actix_web::HttpRequest, @@ -111,6 +111,17 @@ pub async fn payments_create( return api::log_and_return_error_response(err); } + tracing::Span::current().record( + "payment_id", + &payload + .payment_id + .as_ref() + .map(|payment_id_type| payment_id_type.get_payment_intent_id()) + .transpose() + .unwrap_or_default() + .unwrap_or_default(), + ); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -159,7 +170,7 @@ pub async fn payments_create( // tag = "Payments", // operation_id = "Start a Redirection Payment" // )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsStart))] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsStart, payment_id))] pub async fn payments_start( state: web::Data, req: actix_web::HttpRequest, @@ -174,6 +185,7 @@ pub async fn payments_start( }; let locking_action = payload.get_locking_input(flow.clone()); + tracing::Span::current().record("payment_id", &payment_id); Box::pin(api::server_wrap( flow, @@ -223,7 +235,7 @@ pub async fn payments_start( operation_id = "Retrieve a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve, payment_id))] // #[get("/{payment_id}")] pub async fn payments_retrieve( state: web::Data, @@ -241,6 +253,9 @@ pub async fn payments_retrieve( expand_captures: json_payload.expand_captures, ..Default::default() }; + + tracing::Span::current().record("payment_id", &path.to_string()); + let (auth_type, auth_flow) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { Ok(auth) => auth, @@ -291,7 +306,7 @@ pub async fn payments_retrieve( operation_id = "Retrieve a Payment", security(("api_key" = [])) )] -#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve))] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentsRetrieve, payment_id))] // #[post("/sync")] pub async fn payments_retrieve_with_gateway_creds( state: web::Data, @@ -313,6 +328,8 @@ pub async fn payments_retrieve_with_gateway_creds( }; let flow = Flow::PaymentsRetrieve; + tracing::Span::current().record("payment_id", &json_payload.payment_id); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -356,7 +373,7 @@ pub async fn payments_retrieve_with_gateway_creds( operation_id = "Update a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdate))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdate, payment_id))] // #[post("/{payment_id}")] pub async fn payments_update( state: web::Data, @@ -373,6 +390,8 @@ pub async fn payments_update( let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = Some(payment_types::PaymentIdType::PaymentIntentId(payment_id)); let (auth_type, auth_flow) = match auth::get_auth_type_and_flow(req.headers()) { @@ -421,7 +440,7 @@ pub async fn payments_update( operation_id = "Confirm a Payment", security(("api_key" = []), ("publishable_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsConfirm, payment_id))] // #[post("/{payment_id}/confirm")] pub async fn payments_confirm( state: web::Data, @@ -441,6 +460,7 @@ pub async fn payments_confirm( } let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); payload.payment_id = Some(payment_types::PaymentIdType::PaymentIntentId(payment_id)); payload.confirm = Some(true); let header_payload = match payment_types::HeaderPayload::foreign_try_from(req.headers()) { @@ -497,7 +517,7 @@ pub async fn payments_confirm( operation_id = "Capture a Payment", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsCapture))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCapture, payment_id))] // #[post("/{payment_id}/capture")] pub async fn payments_capture( state: web::Data, @@ -505,9 +525,12 @@ pub async fn payments_capture( json_payload: web::Json, path: web::Path, ) -> impl Responder { + let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + let flow = Flow::PaymentsCapture; let payload = payment_types::PaymentsCaptureRequest { - payment_id: path.into_inner(), + payment_id, ..json_payload.into_inner() }; @@ -558,7 +581,7 @@ pub async fn payments_capture( operation_id = "Create Session tokens for a Payment", security(("publishable_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsSessionToken))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsSessionToken, payment_id))] pub async fn payments_connector_session( state: web::Data, req: actix_web::HttpRequest, @@ -567,6 +590,8 @@ pub async fn payments_connector_session( let flow = Flow::PaymentsSessionToken; let payload = json_payload.into_inner(); + tracing::Span::current().record("payment_id", &payload.payment_id); + let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -617,7 +642,7 @@ pub async fn payments_connector_session( // tag = "Payments", // operation_id = "Get Redirect Response for a Payment" // )] -#[instrument(skip_all)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))] pub async fn payments_redirect_response( state: web::Data, req: actix_web::HttpRequest, @@ -628,6 +653,8 @@ pub async fn payments_redirect_response( let (payment_id, merchant_id, connector) = path.into_inner(); let param_string = req.query_string(); + tracing::Span::current().record("payment_id", &payment_id); + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), @@ -676,7 +703,7 @@ pub async fn payments_redirect_response( // tag = "Payments", // operation_id = "Get Redirect Response for a Payment" // )] -#[instrument(skip_all)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))] pub async fn payments_redirect_response_with_creds_identifier( state: web::Data, req: actix_web::HttpRequest, @@ -685,6 +712,8 @@ pub async fn payments_redirect_response_with_creds_identifier( let (payment_id, merchant_id, connector, creds_identifier) = path.into_inner(); let param_string = req.query_string(); + tracing::Span::current().record("payment_id", &payment_id); + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), @@ -715,7 +744,7 @@ pub async fn payments_redirect_response_with_creds_identifier( ) .await } -#[instrument(skip_all)] +#[instrument(skip_all, fields(flow =? Flow::PaymentsRedirect, payment_id))] pub async fn payments_complete_authorize( state: web::Data, req: actix_web::HttpRequest, @@ -726,6 +755,8 @@ pub async fn payments_complete_authorize( let (payment_id, merchant_id, connector) = path.into_inner(); let param_string = req.query_string(); + tracing::Span::current().record("payment_id", &payment_id); + let payload = payments::PaymentsRedirectResponseData { resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(merchant_id.clone()), @@ -774,7 +805,7 @@ pub async fn payments_complete_authorize( operation_id = "Cancel a Payment", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsCancel))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCancel, payment_id))] // #[post("/{payment_id}/cancel")] pub async fn payments_cancel( state: web::Data, @@ -785,6 +816,9 @@ pub async fn payments_cancel( let flow = Flow::PaymentsCancel; let mut payload = json_payload.into_inner(); let payment_id = path.into_inner(); + + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( @@ -919,7 +953,9 @@ pub async fn payments_approve( ) -> impl Responder { let mut payload = json_payload.into_inner(); let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; let flow = Flow::PaymentsApprove; let fpayload = FPaymentsApproveRequest(&payload); @@ -979,7 +1015,9 @@ pub async fn payments_reject( ) -> impl Responder { let mut payload = json_payload.into_inner(); let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; let flow = Flow::PaymentsReject; let fpayload = FPaymentsRejectRequest(&payload); @@ -1120,7 +1158,7 @@ where operation_id = "Increment authorized amount for a Payment", security(("api_key" = [])) )] -#[instrument(skip_all, fields(flow = ?Flow::PaymentsIncrementalAuthorization))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsIncrementalAuthorization, payment_id))] pub async fn payments_incremental_authorization( state: web::Data, req: actix_web::HttpRequest, @@ -1130,6 +1168,9 @@ pub async fn payments_incremental_authorization( let flow = Flow::PaymentsIncrementalAuthorization; let mut payload = json_payload.into_inner(); let payment_id = path.into_inner(); + + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; let locking_action = payload.get_locking_input(flow.clone()); Box::pin(api::server_wrap( From f78d02d981dd7b35f2150f204b327847b811badd Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:09:46 +0530 Subject: [PATCH 283/443] fix(router): multiple incremental_authorizations with kv enabled (#3185) --- crates/diesel_models/src/payment_intent.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 89d99de2d832..df567f583572 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -291,8 +291,9 @@ impl PaymentIntentUpdate { updated_by, surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), - incremental_authorization_allowed, - authorization_count, + incremental_authorization_allowed: incremental_authorization_allowed + .or(source.incremental_authorization_allowed), + authorization_count: authorization_count.or(source.authorization_count), ..source } } From ac5349cd7160f67f7a56f48f54981cf3dc1e5b52 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:11:37 +0530 Subject: [PATCH 284/443] refactor(api_lock): allow api lock on psync only when force sync is true (#3242) --- crates/router/src/routes/payments.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 2ae1d620ccdd..34f41c49cddf 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1271,7 +1271,7 @@ impl GetLockingInput for payment_types::PaymentsRetrieveRequest { lock_utils::ApiIdentifier: From, { match self.resource_id { - payment_types::PaymentIdType::PaymentIntentId(ref id) => { + payment_types::PaymentIdType::PaymentIntentId(ref id) if self.force_sync => { api_locking::LockAction::Hold { input: api_locking::LockingInput { unique_locking_key: id.to_owned(), From bfd8a5a31abb3c95cc9ca21689d5c30a6dc4ce8d Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:57:54 +0530 Subject: [PATCH 285/443] feat(pm_list): add required fields for eps (#3169) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 2 +- crates/router/src/configs/defaults.rs | 228 +- .../router/src/connector/aci/transformers.rs | 8 +- .../src/connector/adyen/transformers.rs | 12 +- .../src/connector/paypal/transformers.rs | 7 +- .../src/connector/stripe/transformers.rs | 17 +- openapi/openapi_spec.json | 8 +- .../adyen_uk.postman_collection.json | 29826 +++++----- .../stripe.postman_collection.json | 48379 ++++++++-------- .../trustpay.postman_collection.json | 13155 +++-- 10 files changed, 46352 insertions(+), 45290 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index d2df44a9b21a..5a894e868a3a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1173,7 +1173,7 @@ pub enum BankRedirectData { }, Eps { /// The billing details for bank redirection - billing_details: BankRedirectBilling, + billing_details: Option, /// The hyperswitch bank code for eps #[schema(value_type = BankNames, example = "triodos_bank")] diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index c90af883d6e3..6c880a4a214f 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4510,14 +4510,240 @@ impl Default for super::settings::RequiredFields { enums::PaymentMethodType::Eps, ConnectorFields { fields: HashMap::from([ + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.bank_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.bank_name".to_string(), + display_name: "bank_name".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ) + ]), + } + ), ( enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Aci, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.country".to_string(), + display_name: "bank_account_country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Globalpay, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), + common: HashMap::from([ + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry { + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + ) + ]) + } + ), + ( + enums::Connector::Mollie, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate:HashMap::new(), common: HashMap::new(), } - ) + ), + ( + enums::Connector::Paypal, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserFullName, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.eps.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.eps.country".to_string(), + display_name: "bank_account_country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Shift4, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate:HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "AT".to_string(), + ] + }, + value: None, + } + )] + ), + common: HashMap::new(), + } + ), ]), }, ), diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index e729eacf9d99..622383ee1567 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -146,10 +146,14 @@ impl ) -> Result { let (item, bank_redirect_data) = value; let payment_data = match bank_redirect_data { - api_models::payments::BankRedirectData::Eps { .. } => { + api_models::payments::BankRedirectData::Eps { country, .. } => { Self::BankRedirect(Box::new(BankRedirectionPMData { payment_brand: PaymentBrand::Eps, - bank_account_country: Some(api_models::enums::CountryAlpha2::AT), + bank_account_country: Some(country.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "eps.country", + }, + )?), bank_account_bank_name: None, bank_account_bic: None, bank_account_iban: None, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 1793e3e07a87..bf24d3b7fd23 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2164,10 +2164,14 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod api_models::payments::BankRedirectData::Eps { bank_name, .. } => Ok( AdyenPaymentMethod::Eps(Box::new(BankRedirectionWithIssuer { payment_type: PaymentType::Eps, - issuer: bank_name - .map(|bank_name| AdyenTestBankNames::try_from(&bank_name)) - .transpose()? - .map(|adyen_bank_name| adyen_bank_name.0), + issuer: Some( + AdyenTestBankNames::try_from(&bank_name.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "eps.bank_name", + }, + )?)? + .0, + ), })), ), api_models::payments::BankRedirectData::Giropay { .. } => Ok( diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 2d3292359d38..fa6ac81c4079 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -290,7 +290,12 @@ fn get_payment_source( bank_name: _, country, } => Ok(PaymentSourceItem::Eps(RedirectRequest { - name: billing_details.get_billing_name()?, + name: billing_details + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "eps.billing_details", + })? + .get_billing_name()?, country_code: country.ok_or(errors::ConnectorError::MissingRequiredField { field_name: "eps.country", })?, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 4338e8f9ff28..ea1b43e22a03 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1083,15 +1083,24 @@ impl From<&payments::BankDebitBilling> for StripeBillingAddress { } impl TryFrom<&payments::BankRedirectData> for StripeBillingAddress { - type Error = errors::ConnectorError; + type Error = error_stack::Report; fn try_from(bank_redirection_data: &payments::BankRedirectData) -> Result { match bank_redirection_data { payments::BankRedirectData::Eps { billing_details, .. - } => Ok(Self { - name: billing_details.billing_name.clone(), - ..Self::default() + } => Ok({ + let billing_data = billing_details.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "billing_details", + }, + )?; + Self { + name: Some(connector_util::BankRedirectBillingData::get_billing_name( + &billing_data, + )?), + ..Self::default() + } }), payments::BankRedirectData::Giropay { billing_details, .. diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 3ffb98e56b95..36cc0cbc012b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -3413,13 +3413,17 @@ "eps": { "type": "object", "required": [ - "billing_details", "bank_name", "country" ], "properties": { "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" + "allOf": [ + { + "$ref": "#/components/schemas/BankRedirectBilling" + } + ], + "nullable": true }, "bank_name": { "$ref": "#/components/schemas/BankNames" diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 797aca78a887..3129182cb02e 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -1,14786 +1,15042 @@ { - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "// Add appropriate profile_id for relevant requests", - "const path = pm.request.url.toString();", - "const isPostRequest = pm.request.method.toString() === \"POST\";", - "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", - "const isPayoutCreation = path.match(/\\/payouts\\/create$/) && isPostRequest;", - "", - "if (isPaymentCreation || isPayoutCreation) {", - " try {", - " const request = JSON.parse(pm.request.body.toJSON().raw);", - "", - " // Attach profile_id", - " const profile_id = isPaymentCreation", - " ? pm.collectionVariables.get(\"payment_profile_id\")", - " : pm.collectionVariables.get(\"payout_profile_id\");", - " request[\"profile_id\"] = profile_id;", - "", - " // Attach routing", - " const routing = { type: \"single\", data: \"adyen\" };", - " request[\"routing\"] = routing;", - "", - " let updatedRequest = {", - " mode: \"raw\",", - " raw: JSON.stringify(request),", - " options: {", - " raw: {", - " language: \"json\",", - " },", - " },", - " };", - " pm.request.body.update(updatedRequest);", - " } catch (error) {", - " console.error(\"Failed to inject profile_id in the request\");", - " console.error(error);", - " }", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", - "}", - "", - "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", - "" - ], - "type": "text/javascript" - } - } - ], - "item": [ - { - "name": "Health check", - "item": [ - { - "name": "New Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Flow Testcases", - "item": [ - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"},{\"country\":\"GB\",\"business\":\"payouts\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id", - "if (jsonData?.profile_id) {", - " pm.collectionVariables.set(\"payment_profile_id\", jsonData.profile_id);", - " console.log(", - " \"- use {{payment_profile_id}} as collection variable for value\",", - " jsonData.profile_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payout Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id", - "if (jsonData?.profile_id) {", - " pm.collectionVariables.set(\"payout_profile_id\", jsonData.profile_id);", - " console.log(", - " \"- use {{payout_profile_id}} as collection variable for value\",", - " jsonData.profile_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payouts - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payouts/create - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Validate if status is successful", - "// if (jsonData?.status) {", - "// pm.test(\"[POST]::/payouts/create - Content check if value for 'status' matches 'success'\",", - "// function () {", - "// pm.expect(jsonData.status).to.eql(\"success\");", - "// },", - "// );", - "// }", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", - "if (jsonData?.payout_id) {", - " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", - " console.log(", - " \"- use {{payout_id}} as collection variable for value\",", - " jsonData.payout_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}" - }, - "url": { - "raw": "{{baseUrl}}/payouts/create", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payouts", - "create" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payouts - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payouts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payouts/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payouts/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", - "if (jsonData?.payout_id) {", - " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", - " console.log(", - " \"- use {{payout_id}} as collection variable for value\",", - " jsonData.payout_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payouts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payouts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payout_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ - { - "name": "Scenario22-Create Gift Card payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Create 3DS payment with confrm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9-Refund full payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario10-Create a mandate and recurring payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario11-Partial refund", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"1000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario12-Bank Redirect-sofort", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario13-Bank Redirect-eps", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"eps\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", - " },", - " );", - "}", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario14-Refund recurring payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario15-Bank Redirect-giropay", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario16-Bank debit-ach", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario17-Bank debit-Bacs", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "", - "// Response body should have value \"bacs\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", - " },", - " );", - "}", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"processing\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"processing\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario18-Bank Redirect-Trustly", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario19-Add card flow", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"pending\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"24\");", - " },", - " );", - "}", - "", - "// Response body should have value \"24\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ - { - "name": "Scenario10-Create Gift Card payment where it fails due to insufficient balance", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Response body should have error message as \"Insufficient balance in the payment method\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"Insufficient balance in the payment method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":14100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":14100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with Invalid card details", - "item": [ - { - "name": "Payments - Create(Invalid card number)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"123456\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp month)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Card Expired'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Card Expired\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"01\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp Year)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(invalid CVV)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid card_cvc length'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid card_cvc length\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Confirming the payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Capture greater amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Capture the succeeded payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the success_slash_failure payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment with greater capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Refund exceeds amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"The refund amount exceeds the amount captured\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario8-Refund for unsuccessful payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(", - " \"This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured\",", - " );", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9-Create a recurring payment with greater mandate amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Mandate Validation Failed'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Mandate Validation Failed\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8040,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - }, - { - "name": "Scenario10-Create payouts using unsupported methods", - "item": [ - { - "name": "ACH Payouts - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", - " pm.response.to.be.clientError;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", - "if (jsonData?.payout_id) {", - " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", - " console.log(", - " \"- use {{payout_id}} as collection variable for value\",", - " jsonData.payout_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\",\"vendor_details\":{\"account_type\":\"custom\",\"business_type\":\"individual\",\"business_profile_mcc\":5045,\"business_profile_url\":\"https://www.pastebin.com\",\"business_profile_name\":\"pT\",\"company_address_line1\":\"address_full_match\",\"company_address_line2\":\"Kimberly Way\",\"company_address_postal_code\":\"31062\",\"company_address_city\":\"Milledgeville\",\"company_address_state\":\"GA\",\"company_phone\":\"+16168205366\",\"company_tax_id\":\"000000000\",\"company_owners_provided\":false,\"capabilities_card_payments\":true,\"capabilities_transfers\":true},\"individual_details\":{\"tos_acceptance_date\":1680581051,\"tos_acceptance_ip\":\"103.159.11.202\",\"individual_dob_day\":\"01\",\"individual_dob_month\":\"01\",\"individual_dob_year\":\"1901\",\"individual_id_number\":\"000000000\",\"individual_ssn_last_4\":\"0000\",\"external_account_account_holder_type\":\"individual\"}},\"confirm\":true,\"auto_fulfill\":true}" - }, - "url": { - "raw": "{{baseUrl}}/payouts/create", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payouts", - "create" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Bacs Payouts - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", - " pm.response.to.be.clientError;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", - "if (jsonData?.payout_id) {", - " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", - " console.log(", - " \"- use {{payout_id}} as collection variable for value\",", - " jsonData.payout_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"adyen\"],\"business_label\":\"abcd\",\"business_country\":\"US\"}" - }, - "url": { - "raw": "{{baseUrl}}/payouts/create", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payouts", - "create" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - } - ] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "info": { - "_postman_id": "de5a144d-d247-4751-a8f3-ecfa9832641c", - "name": "adyen_uk", - "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "variable": [ - { - "key": "baseUrl", - "value": "https://integ.hyperswitch.io", - "type": "string" - }, - { - "key": "admin_api_key", - "value": "", - "type": "string" - }, - { - "key": "api_key", - "value": "", - "type": "string" - }, - { - "key": "merchant_id", - "value": "" - }, - { - "key": "payment_id", - "value": "" - }, - { - "key": "customer_id", - "value": "" - }, - { - "key": "mandate_id", - "value": "" - }, - { - "key": "payment_method_id", - "value": "" - }, - { - "key": "refund_id", - "value": "" - }, - { - "key": "payout_id", - "value": "", - "type": "string" - }, - { - "key": "merchant_connector_id", - "value": "" - }, - { - "key": "client_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_api_key", - "value": "", - "type": "string" - }, - { - "key": "connector_key1", - "value": "" - }, - { - "key": "publishable_key", - "value": "", - "type": "string" - }, - { - "key": "payment_token", - "value": "", - "type": "string" - }, - { - "key": "gateway_merchant_id", - "value": "", - "type": "string" - }, - { - "key": "certificate", - "value": "", - "type": "string" - }, - { - "key": "certificate_keys", - "value": "", - "type": "string" - }, - { - "key": "api_key_id", - "value": "" - }, - { - "key": "connector_api_secret", - "value": "", - "type": "string" - }, - { - "key": "payment_profile_id", - "value": "", - "type": "string" - }, - { - "key": "payout_profile_id", - "value": "", - "type": "string" - } - ] -} + "info": { + "_postman_id": "707f91ae-7be8-4899-b9d3-d069a71470ab", + "name": "adyen_uk", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "27028646" + }, + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"},{\"country\":\"GB\",\"business\":\"payouts\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payment_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payment_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payment_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":false,\"installment_payment_enabled\":false},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payout Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set profile_id as variable for jsonData.payout_profile_id", + "if (jsonData?.profile_id) {", + " pm.collectionVariables.set(\"payout_profile_id\", jsonData.profile_id);", + " console.log(", + " \"- use {{payout_profile_id}} as collection variable for value\",", + " jsonData.profile_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_profile_id}}, as jsonData.profile_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"GB\",\"business_label\":\"payouts\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payouts/create - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payouts/create - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Validate if status is successful", + "// if (jsonData?.status) {", + "// pm.test(\"[POST]::/payouts/create - Content check if value for 'status' matches 'success'\",", + "// function () {", + "// pm.expect(jsonData.status).to.eql(\"success\");", + "// },", + "// );", + "// }", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1,\"currency\":\"EUR\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"iban\":\"NL46TEST0136169112\",\"bic\":\"ABNANL2A\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payouts - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payouts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payouts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payouts/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payouts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payout_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario22-Create Gift Card payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario23-Create PM list for dynamic fields", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for Merchants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "if (", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"payment_method_data.bank_redirect.eps.bank_name\"]", + " )", + " )", + ") {", + " pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'payment_method_data.bank_redirect.eps.bank_name'\",", + " function () {", + " pm.expect(jsonData.payment_methods).to.be.an('array').that.is.not.empty;", + " }", + " );", + "} else {", + " console.log(", + " \"INFO - EPS required field does not contain 'payment_method_data.bank_redirect.eps.bank_name'.\"", + " );", + "}", + "", + "pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'payment_method_data.bank_redirect.eps.bank_name'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"payment_method_data.bank_redirect.eps.bank_name\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + "", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Create 3DS payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Create 3DS payment with confrm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4917610000000000\",\"card_exp_month\":\"03\",\"card_exp_year\":\"30\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Refund full payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario10-Create a mandate and recurring payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Recurring Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario11-Partial refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"RETURN\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario12-Bank Redirect-sofort", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"sofort\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario13-Bank Redirect-eps", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"eps\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " },", + " );", + "}", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"AT\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario14-Refund recurring payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Recurring Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"DUPLICATE\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario15-Bank Redirect-giropay", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario16-Bank debit-ach", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"ach\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"US\",\"name\":\"A. Klaassen\",\"email\":\"example@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario17-Bank debit-Bacs", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":100,\"currency\":\"GBP\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"off_session\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"GB\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "", + "// Response body should have value \"bacs\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'bacs'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"bacs\");", + " },", + " );", + "}", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_debit\",\"payment_method_type\":\"bacs\",\"payment_method_data\":{\"bank_debit\":{\"bacs_bank_debit\":{\"account_number\":\"40308669\",\"routing_number\":\"121000358\",\"sort_code\":\"560036\",\"shopper_email\":\"example@gmail.com\",\"card_holder_name\":\"joseph Doe\",\"bank_account_holder_name\":\"David Archer\",\"billing_details\":{\"houseNumberOrName\":\"50\",\"street\":\"Test Street\",\"city\":\"Amsterdam\",\"stateOrProvince\":\"NY\",\"postalCode\":\"12010\",\"country\":\"GB\",\"name\":\"A. Klaassen\",\"email\":\"abcd@gmail.com\"},\"reference\":\"daslvcgbaieh\"}}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"processing\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'processing'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"processing\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario18-Bank Redirect-Trustly", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"FI\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'trustly'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"trustly\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"trustly\",\"payment_method_data\":{\"bank_redirect\":{\"trustly\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"FI\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario19-Add card flow", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"FRAUD\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'pending'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"pending\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario20-Pass Invalid CVV for save card flow and verify failed payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"24\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario21-Don't Pass CVV for save card flow and verify failed payment Copy", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches '24'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"24\");", + " },", + " );", + "}", + "", + "// Response body should have value \"24\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'CVC Declined'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"CVC Declined\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"adyen\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario10-Create Gift Card payment where it fails due to insufficient balance", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have error message as \"Insufficient balance in the payment method\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"Insufficient balance in the payment method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":14100,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":14100,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"gift_card\",\"payment_method_type\":\"givex\",\"payment_method_data\":{\"gift_card\":{\"givex\":{\"number\":\"6364530000000000\",\"cvc\":\"122222\"}}},\"routing\":{\"type\":\"single\",\"data\":\"adyen\"},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with Invalid card details", + "item": [ + { + "name": "Payments - Create(Invalid card number)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"123456\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp month)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Card Expired'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Card Expired\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"01\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp Year)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(invalid CVV)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid card_cvc length'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid card_cvc length\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Confirming the payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Capture greater amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Capture the succeeded payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the success_slash_failure payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Create 3DS payment with greater capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Refund exceeds amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"The refund amount exceeds the amount captured\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario8-Refund for unsuccessful payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(", + " \"This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured\",", + " );", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Create a recurring payment with greater mandate amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Recurring Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Mandate Validation Failed'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Mandate Validation Failed\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8040,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario10-Create payouts using unsupported methods", + "item": [ + { + "name": "ACH Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":10000,\"currency\":\"USD\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"Doest John\",\"phone\":\"6168205366\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payout request\",\"connector\":[\"adyen\"],\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_routing_number\":\"110000000\",\"bank_account_number\":\"000123456789\",\"bank_name\":\"Stripe Test Bank\",\"bank_country_code\":\"US\",\"bank_city\":\"California\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"Doest\",\"last_name\":\"John\"},\"phone\":{\"number\":\"6168205366\",\"country_code\":\"1\"}},\"entity_type\":\"Individual\",\"recurring\":false,\"metadata\":{\"ref\":\"123\",\"vendor_details\":{\"account_type\":\"custom\",\"business_type\":\"individual\",\"business_profile_mcc\":5045,\"business_profile_url\":\"https://www.pastebin.com\",\"business_profile_name\":\"pT\",\"company_address_line1\":\"address_full_match\",\"company_address_line2\":\"Kimberly Way\",\"company_address_postal_code\":\"31062\",\"company_address_city\":\"Milledgeville\",\"company_address_state\":\"GA\",\"company_phone\":\"+16168205366\",\"company_tax_id\":\"000000000\",\"company_owners_provided\":false,\"capabilities_card_payments\":true,\"capabilities_transfers\":true},\"individual_details\":{\"tos_acceptance_date\":1680581051,\"tos_acceptance_ip\":\"103.159.11.202\",\"individual_dob_day\":\"01\",\"individual_dob_month\":\"01\",\"individual_dob_year\":\"1901\",\"individual_id_number\":\"000000000\",\"individual_ssn_last_4\":\"0000\",\"external_account_account_holder_type\":\"individual\"}},\"confirm\":true,\"auto_fulfill\":true}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Bacs Payouts - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payouts/create - Status code is 4xx\", function () {", + " pm.response.to.be.clientError;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payouts/create - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payouts/create - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payout_id as variable for jsonData.payout_id", + "if (jsonData?.payout_id) {", + " pm.collectionVariables.set(\"payout_id\", jsonData.payout_id);", + " console.log(", + " \"- use {{payout_id}} as collection variable for value\",", + " jsonData.payout_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payout_id}}, as jsonData.payout_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1,\"currency\":\"GBP\",\"customer_id\":\"payout_customer\",\"email\":\"payout_customer@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payout request\",\"payout_type\":\"bank\",\"payout_method_data\":{\"bank\":{\"bank_sort_code\":\"231470\",\"bank_account_number\":\"28821822\",\"bank_name\":\"Deutsche Bank\",\"bank_country_code\":\"NL\",\"bank_city\":\"Amsterdam\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"CA\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"entity_type\":\"Individual\",\"recurring\":true,\"metadata\":{\"ref\":\"123\"},\"confirm\":true,\"auto_fulfill\":true,\"connector\":[\"adyen\"],\"business_label\":\"abcd\",\"business_country\":\"US\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payouts/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payouts", + "create" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Add appropriate profile_id for relevant requests", + "const path = pm.request.url.toString();", + "const isPostRequest = pm.request.method.toString() === \"POST\";", + "const isPaymentCreation = path.match(/\\/payments$/) && isPostRequest;", + "const isPayoutCreation = path.match(/\\/payouts\\/create$/) && isPostRequest;", + "", + "if (isPaymentCreation || isPayoutCreation) {", + " try {", + " const request = JSON.parse(pm.request.body.toJSON().raw);", + "", + " // Attach profile_id", + " const profile_id = isPaymentCreation", + " ? pm.collectionVariables.get(\"payment_profile_id\")", + " : pm.collectionVariables.get(\"payout_profile_id\");", + " request[\"profile_id\"] = profile_id;", + "", + " // Attach routing", + " const routing = { type: \"single\", data: \"adyen\" };", + " request[\"routing\"] = routing;", + "", + " let updatedRequest = {", + " mode: \"raw\",", + " raw: JSON.stringify(request),", + " options: {", + " raw: {", + " language: \"json\",", + " },", + " },", + " };", + " pm.request.body.update(updatedRequest);", + " } catch (error) {", + " console.error(\"Failed to inject profile_id in the request\");", + " console.error(error);", + " }", + "}", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "https://integ.hyperswitch.io", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "payout_id", + "value": "", + "type": "string" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "", + "type": "string" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + }, + { + "key": "payment_profile_id", + "value": "", + "type": "string" + }, + { + "key": "payout_profile_id", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index e158ccd1a5eb..0f7f146b9d11 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -1,24072 +1,24309 @@ { - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", - "}", - "", - "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", - "" - ], - "type": "text/javascript" - } - } - ], - "item": [ - { - "name": "Health check", - "item": [ - { - "name": "New Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "MerchantAccounts", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", - " console.log(", - " \"- use {{organization_id}} as collection variable for value\",", - " jsonData.organization_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/accounts - Organization id is generated\",", - " function () {", - " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "Merchant Account - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/accounts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Retrieve a merchant account details." - }, - "response": [] - }, - { - "name": "Merchant Account - List", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - "list" - ], - "query": [ - { - "key": "organization_id", - "value": "{{organization_id}}" - } - ], - "variable": [ - { - "key": "organization_id", - "value": "{{organization_id}}", - "description": "(Required) - Organization id" - } - ] - }, - "description": "List merchant accounts for an organization" - }, - "response": [] - }, - { - "name": "Merchant Account - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/accounts/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"{{merchant_id}}\",\"merchant_name\":\"NewAge Retailer\",\"locker_id\":\"m0010\",\"merchant_details\":{\"primary_contact_person\":\"joseph Test\",\"primary_email\":\"josephTest@test.com\",\"primary_phone\":\"veniam aute officia ullamco esse\",\"secondary_contact_person\":\"joseph Test2\",\"secondary_email\":\"josephTest2@test.com\",\"secondary_phone\":\"proident adipisicing officia nulla\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"parent_merchant_id\":\"xkkdf909012sdjki2dkh5sdf\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/accounts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" - }, - "response": [] - } - ] - }, - { - "name": "API Key", - "item": [ - { - "name": "Create API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Update API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":null,\"description\":\"My very awesome API key\",\"expiration\":null}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - ":api_key_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - }, - { - "key": "api_key_id", - "value": "{{api_key_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Retrieve API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - ":api_key_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - }, - { - "key": "api_key_id", - "value": "{{api_key_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "List API Keys", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/api_keys/:merchant_id/list - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/api_keys/:merchant_id/list - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/list", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - "list" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Delete API Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id", - ":api-key" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - }, - { - "key": "api-key", - "value": "{{api_key_id}}" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "PaymentConnectors", - "item": [ - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "", - "// Validate if the connector label is the one that is passed in the request", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", - " function () {", - " pm.expect(jsonData.connector_label).to.eql(\"first_stripe_connector\")", - " },", - ");" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_stripe_connector\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payment Connector - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}", - "description": "(Required) The unique identifier for the payment connector" - } - ] - }, - "description": "Retrieve Payment Connector details." - }, - "response": [] - }, - { - "name": "Payment Connector - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) { }", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "", - "// Validate if the connector label is the one that is passed in the request", - "pm.test(", - " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", - " function () {", - " pm.expect(jsonData.connector_label).to.eql(\"updated_stripe_connector\")", - " },", - ");" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"connector_label\":\"updated_stripe_connector\",\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}" - } - ] - }, - "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" - }, - "response": [] - }, - { - "name": "List Connectors by MID", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Delete", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}" - } - ] - }, - "description": "Delete or Detach a Payment Connector from Merchant Account" - }, - "response": [] - }, - { - "name": "Merchant Account - Delete", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[DELETE]::/accounts/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/accounts/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Response Validation", - "const schema = {", - " type: \"object\",", - " description: \"Merchant Account\",", - " required: [\"merchant_id\", \"deleted\"],", - " properties: {", - " merchant_id: {", - " type: \"string\",", - " description: \"The identifier for the MerchantAccount object.\",", - " maxLength: 255,", - " example: \"y3oqhf46pyzuxjbcn2giaqnb44\",", - " },", - " deleted: {", - " type: \"boolean\",", - " description:", - " \"Indicates the deletion status of the Merchant Account object.\",", - " example: true,", - " },", - " },", - "};", - "", - "// Validate if response matches JSON schema", - "pm.test(\"[DELETE]::/accounts/:id - Schema is valid\", function () {", - " pm.response.to.have.jsonSchema(schema, {", - " unknownFormats: [\"int32\", \"int64\", \"float\", \"double\"],", - " });", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/accounts/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Delete a Merchant Account" - }, - "response": [] - } - ] - }, - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "/*", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(\"- use {{api_key}} as collection variable for value\",jsonData.api_key);", - "} else {", - " console.log('INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.');", - "};", - "*/", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"failed\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Response body should have an error message", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error_message' is not 'null'\",", - " function () {", - " pm.expect(jsonData.error_message).is.not.null;", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payment Connector - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors", - ":connector_id" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}" - }, - { - "key": "connector_id", - "value": "{{merchant_connector_id}}" - } - ] - }, - "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" - }, - "response": [] - }, - { - "name": "Payments - Create-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "", - "// Response body should have \"profile_id\" and not \"null\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'profile_id' exists and is not 'null'\",", - " function () {", - " pm.expect(typeof jsonData.profile_id !== \"undefined\").to.be.true;", - " pm.expect(jsonData.profile_id).is.not.null;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Customers", - "description": "Create a Customer entity which you can use to store and retrieve specific customers' data and payment methods.", - "item": [ - { - "name": "Create Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Response body should have \"customer_id\"", - "pm.test(", - " \"[POST]::/customers - Content check if 'customer_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.customer_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have a minimum length of \"1\" for \"customer_id\"", - "if (jsonData?.customer_id) {", - " pm.test(", - " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '1'\",", - " function () {", - " pm.expect(jsonData.customer_id.length).is.at.least(1);", - " },", - " );", - "}", - "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(", - " \"- use {{customer_id}} as collection variable for value\",", - " jsonData.customer_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"First customer\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/customers", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers" - ] - }, - "description": "Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details." - }, - "response": [] - }, - { - "name": "List Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "", - "// Response body should have a minimum length of \"1\" for \"customer_id\"", - "if (jsonData?.customer_id) {", - " pm.test(", - " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '2'\",", - " function () {", - " pm.expect(jsonData.customer_id.length).is.at.least(2);", - " },", - " );", - "}", - "", - "", - "// Define the regular expression pattern to match customer_id", - "var customerIdPattern = /^[a-zA-Z0-9_]+$/;", - "", - "// Define an array to store the validation results", - "var validationResults = [];", - "", - "// Iterate through the JSON array", - "jsonData.forEach(function(item, index) {", - " if (item.hasOwnProperty(\"customer_id\")) {", - " if (customerIdPattern.test(item.customer_id)) {", - " validationResults.push(\"customer_id \" + item.customer_id + \" is valid.\");", - " } else {", - " validationResults.push(\"customer_id \" + item.customer_id + \" is not valid.\");", - " }", - " } else {", - " validationResults.push(\"customer_id is missing for item at index \" + index);", - " }", - "});", - "", - "// Check if any customer_id is not valid and fail the test if necessary", - "if (validationResults.some(result => !result.includes(\"is valid\"))) {", - " pm.test(\"Customer IDs validation failed: \" + validationResults.join(\", \"), function() {", - " pm.expect(false).to.be.true;", - " });", - "} else {", - " pm.test(\"All customer IDs are valid: \" + validationResults.join(\", \"), function() {", - " pm.expect(true).to.be.true;", - " });", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/list", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - "list" - ] - } - }, - "response": [] - }, - { - "name": "Retrieve Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/customers/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/customers/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/customers/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{customer_id}}", - "description": "(Required) unique customer id" - } - ] - }, - "description": "Retrieve a customer's details." - }, - "response": [] - }, - { - "name": "Update Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/customers/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/customers/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/customers/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"email\":\"JohnTest@test.com\",\"name\":\"John Test\",\"phone_country_code\":\"+65\",\"phone\":\"888888888\",\"description\":\"First customer\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/customers/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{customer_id}}", - "description": "(Required) unique customer id" - } - ] - }, - "description": "Updates the customer's details in a customer object." - }, - "response": [] - }, - { - "name": "Ephemeral Key", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/ephemeral_keys - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/ephemeral_keys - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"customer_id\":\"{{customer_id}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/ephemeral_keys", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "ephemeral_keys" - ] - } - }, - "response": [] - }, - { - "name": "Delete Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[DELETE]::/customers/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[DELETE]::/customers/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[DELETE]::/customers/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{customer_id}}", - "description": "(Required) unique customer id" - } - ] - }, - "description": "Delete a customer record." - }, - "response": [] - } - ] - }, - { - "name": "Payments", - "description": "Process and manage payments across wide range of payment processors using the Unified Payments API.", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Session Token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/session_tokens - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/session_tokens - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "const responseJson = pm.response.json();", - "", - "// Verify if the wallet_name in the response matches 'apple_pay'", - "pm.test(", - " \"[POST]::/payments/session_tokens - Verify wallet_name is 'apple_pay'\",", - " function () {", - " pm.expect(responseJson.session_token[0].wallet_name).to.eql(\"apple_pay\");", - " },", - ");", - "", - "// Verify if the wallet_name in the response matches 'google_pay'", - "pm.test(", - " \"[POST]::/payments/session_tokens - Verify wallet_name is 'google_pay'\",", - " function () {", - " pm.expect(responseJson.session_token[1].wallet_name).to.eql(\"google_pay\");", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"payment_id\":\"{{payment_id}}\",\"wallets\":[],\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/session_tokens", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - "session_tokens" - ] - } - }, - "response": [] - }, - { - "name": "Payments - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Confirm (Through Client Secret)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Create Again", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm Again (Through API Key)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Create Yet Again", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - }, - { - "name": "Payment-List", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/list - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/payments/list - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "api-key", - "value": "snd_0b8e1deb82f241eca47617afb1398858" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/list", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - "list" - ], - "query": [ - { - "key": "customer_id", - "value": "", - "disabled": true - }, - { - "key": "starting_after", - "value": "", - "disabled": true - }, - { - "key": "ending_before", - "value": "", - "disabled": true - }, - { - "key": "limit", - "value": "100", - "disabled": true - }, - { - "key": "created", - "value": "", - "disabled": true - }, - { - "key": "created.lt", - "value": "", - "disabled": true - }, - { - "key": "created.gt", - "value": "", - "disabled": true - }, - { - "key": "created.lte", - "value": "", - "disabled": true - }, - { - "key": "created_gte", - "value": "", - "disabled": true - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Refunds", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"reason\":\"Paid by mistake\",\"metadata\":{\"udf1\":\"value2\",\"new_customer\":\"false\",\"login_date\":\"2019-09-1T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To update the properties of a Refund object. This may include attaching a reason for the refund or metadata fields" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "PaymentMethods", - "item": [ - { - "name": "P_Create Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Response body should have \"customer_id\"", - "pm.test(", - " \"[POST]::/customers - Content check if 'customer_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.customer_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have a minimum length of \"1\" for \"customer_id\"", - "if (jsonData?.customer_id) {", - " pm.test(", - " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '1'\",", - " function () {", - " pm.expect(jsonData.customer_id.length).is.at.least(1);", - " },", - " );", - "}", - "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(", - " \"- use {{customer_id}} as collection variable for value\",", - " jsonData.customer_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"First customer\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/customers", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers" - ] - }, - "description": "Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details." - }, - "response": [] - }, - { - "name": "PaymentMethods - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payment_methods - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payment_methods - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_method_id as variable for jsonData.payment_method_id", - "if (jsonData?.payment_method_id) {", - " pm.collectionVariables.set(\"payment_method_id\", jsonData.payment_method_id);", - " console.log(", - " \"- use {{payment_method_id}} as collection variable for value\",", - " jsonData.payment_method_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_method_id}}, as jsonData.payment_method_id is undefined.\",", - " );", - "}", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(", - " \"- use {{customer_id}} as collection variable for value\",", - " jsonData.customer_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_issuer\":\"Visa\",\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"John Doe\"},\"customer_id\":\"{{customer_id}}\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payment_methods" - ] - }, - "description": "To create a payment method against a customer object. In case of cards, this API could be used only by PCI compliant merchants" - }, - "response": [] - }, - { - "name": "List payment methods for a Merchant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/payment_methods/:merchant_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - "payment_methods" - ], - "query": [ - { - "key": "client_secret", - "value": "{{client_secret}}" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular merchant id." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(", - " \"- use {{customer_id}} as collection variable for value\",", - " jsonData.customer_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[GET]::/payment_methods/:customer_id - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[GET]::/payment_methods/:customer_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(", - " \"payment_token\",", - " jsonData.customer_payment_methods[0].payment_token,", - " );", - " console.log(", - " \"- use {{payment_token}} as collection variable for value\",", - " jsonData.customer_payment_methods[0].payment_token,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - } - ] - }, - { - "name": "Flow Testcases", - "item": [ - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}" - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2a-Create payment with confirm false card holder name null", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2b-Create payment with confirm false card holder name empty", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Create payment with Manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4a-Create payment with manual_multiple capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"cancelled\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"cancelled\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Create 3DS payment with confrm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario8-Create a failure card payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"card_declined\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", - " },", - " );", - "}", - "", - "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"failed\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"card_declined\" for \"error_code\"", - "if (jsonData?.error_code) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", - " function () {", - " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", - " },", - " );", - "}", - "", - "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", - "if (jsonData?.error_message) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", - " function () {", - " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9-Refund full payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9a-Partial refund", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"1000\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"refunds\"", - "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", - " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario10-Create a mandate and recurring payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario11-Refund recurring payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"payment_method_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario12-BNPL-klarna", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"klarna\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'klarna'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"klarna\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"klarna_redirect\":{\"issuer_name\":\"stripe\",\"billing_email\":\"arjun.karthik@juspay.in\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario13-BNPL-afterpay", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"SE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"SE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"afterpay_clearpay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'afterpay_clearpay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"afterpay_clearpay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"afterpay_clearpay_redirect\":{\"billing_name\":\"Akshaya\",\"billing_email\":\"example@example.com\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario14-BNPL-affirm", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"affirm\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'affirm'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"affirm\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"affirm_redirect\":{\"issuer_name\":\"affirm\",\"billing_email\":\"user-us@example.com\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario15-Bank Redirect-Ideal", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"ideal\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario16-Bank Redirect-sofort", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_noe_lb_fur_niederosterreich_u_wien\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario17-Bank Redirect-eps", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"eps\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_oberosterreich_salzburg_steiermark\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario18-Bank Redirect-giropay", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario19-Bank Transfer-ach", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://hs-payments-test.netlify.app/payments\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.type\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.type' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", - " },", - " );", - "}", - "", - "// Response body should have value \"display_bank_transfer_information\" for \"next_action.type\"", - "if (jsonData?.next_action.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'display_bank_transfer_information'\",", - " function () {", - " pm.expect(jsonData.next_action.type).to.eql(", - " \"display_bank_transfer_information\",", - " );", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"bank_transfer\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_transfer\":{\"ach_bank_transfer\":{\"billing_details\":{\"email\":\"johndoe@example.com\"}}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario20-Bank Debit-ach", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1800,\"currency\":\"USD\",\"confirm\":true,\"business_label\":\"default\",\"capture_method\":\"automatic\",\"connector\":[\"stripe\"],\"customer_id\":\"klarna\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"authentication_type\":\"three_ds\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"return_url\":\"https://google.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"setup_future_usage\":\"off_session\",\"business_country\":\"US\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"online\",\"accepted_at\":\"2022-09-10T10:11:12Z\",\"online\":{\"ip_address\":\"123.32.25.123\",\"user_agent\":\"Mozilla/5.0 (Linux; Android 12; SM-S906N Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36\"}},\"mandate_type\":{\"single_use\":{\"amount\":6540,\"currency\":\"USD\"}}},\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"billing_details\":{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"},\"account_number\":\"000123456789\",\"routing_number\":\"110000000\"}}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":1800,\"account_name\":\"transaction_processing\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario21-Wallet-Wechatpay", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.type\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.type' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"ach\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'we_chat_pay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"we_chat_pay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"qr_code_information\" for \"next_action.type\"", - "if (jsonData?.next_action.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'qr_code_information'\",", - " function () {", - " pm.expect(jsonData.next_action.type).to.eql(\"qr_code_information\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"wallet\",\"payment_method_type\":\"we_chat_pay\",\"payment_method_data\":{\"wallet\":{\"we_chat_pay_qr\":{}}},\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario22- Update address and List Payment method", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - "})};", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List Payment Methods for a Merchant", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Parse the response body as JSON", - "var responseBody = pm.response.json();", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"card\";", - " });", - "});", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"pay_later\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'pay_later'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"pay_later\";", - " });", - "});", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"wallet\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'wallet'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"wallet\";", - " });", - "});", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_debit\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_debit'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_debit\";", - " });", - "});", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_transfer\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_transfer'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_transfer\";", - " });", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - "payment_methods" - ], - "query": [ - { - "key": "client_secret", - "value": "{{client_secret}}" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular merchant id." - }, - "response": [] - }, - { - "name": "Payments - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "", - "// Parse the JSON response", - "var jsonData = pm.response.json();", - "", - "// Check if the 'currency' is equal to \"EUR\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'currency' matches 'EUR' \", function () {", - " pm.expect(jsonData.currency).to.eql(\"EUR\");", - "});", - "", - "// Extract the \"country\" field from the JSON data", - "var country = jsonData.billing.address.country;", - "", - "// Check if the country is \"NL\"", - "pm.test(\"[POST]::/payments/:id -Content Check if billing 'Country' matches NL (Netherlands)\", function () {", - " pm.expect(country).to.equal(\"NL\");", - "});", - "", - "var country1 = jsonData.shipping.address.country;", - "", - "// Check if the country is \"NL\"", - "pm.test(\"[POST]::/payments/:id -Content Check if shipping 'Country' matches NL (Netherlands)\", function () {", - " pm.expect(country1).to.equal(\"NL\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"currency\":\"EUR\",\"shipping\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}},\"billing\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " - }, - "response": [] - }, - { - "name": "List Payment Methods for a Merchant-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "", - "// Parse the response body as JSON", - "var responseBody = pm.response.json();", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"card\";", - " });", - "});", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"ideal\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id - Content Check if payment_method matches 'ideal'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"ideal\";", - " });", - "});", - "", - "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_redirect\"", - "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_redirect'\", function () {", - " var paymentMethods = responseBody.payment_methods;", - " var cardPaymentMethod = paymentMethods.find(function (method) {", - " return method.payment_method == \"bank_redirect\";", - " });", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - "payment_methods" - ], - "query": [ - { - "key": "client_secret", - "value": "{{client_secret}}" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular merchant id." - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "//// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - "})};", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"Example\",\"email\":\"guest@example.com\"},\"bank_name\":\"ing\"}}},\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"125.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"multi_use\":{\"amount\":7000,\"currency\":\"USD\",\"start_date\":\"2023-04-21T00:00:00Z\",\"end_date\":\"2023-05-21T00:00:00Z\",\"metadata\":{\"frequency\":\"13\"}}}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - "})};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario23- Update Amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - "})};", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Update", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "", - "// Parse the JSON response", - "var jsonData = pm.response.json();", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", - "", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "//// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - "})};", - "", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", - "", - "//// Response body should have value \"amount_received\" for \"1000\"", - "if (jsonData?.amount_received) {", - "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", - " pm.expect(jsonData.amount_received).to.eql(1000);", - "})};", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - }, - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - }, - { - "key": "publishable_key", - "value": "", - "type": "text", - "disabled": true - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "//// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - "})};", - "", - "", - "// Check if the 'amount' is equal to \"1000\"", - "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", - " pm.expect(jsonData.amount).to.eql(1000);", - "});", - "", - "//// Response body should have value \"amount_received\" for \"1000\"", - "if (jsonData?.amount_received) {", - "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", - " pm.expect(jsonData.amount_received).to.eql(1000);", - "})};", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario24-Add card flow", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " })};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4111111111111111\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario25-Don't Pass CVV for save card flow and verifysuccess payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"adyen\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " })};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - } - ] - }, - { - "name": "Scenario26-Save card payment with manual capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "List payment methods for a Customer", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "if (jsonData?.customer_payment_methods[0]?.payment_token) {", - " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", - " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "customers", - ":customer_id", - "payment_methods" - ], - "query": [ - { - "key": "accepted_country", - "value": "co", - "disabled": true - }, - { - "key": "accepted_country", - "value": "pa", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "voluptate ea", - "disabled": true - }, - { - "key": "accepted_currency", - "value": "exercitation", - "disabled": true - }, - { - "key": "minimum_amount", - "value": "100", - "disabled": true - }, - { - "key": "maximum_amount", - "value": "10000000", - "disabled": true - }, - { - "key": "recurring_payment_enabled", - "value": "true", - "disabled": true - }, - { - "key": "installment_payment_enabled", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "customer_id", - "value": "{{customer_id}}", - "description": "//Pass the customer id" - } - ] - }, - "description": "To filter and list the applicable payment methods for a particular Customer ID" - }, - "response": [] - }, - { - "name": "Save card payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx ", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", - "});", - "", - "// Validate if response has JSON Body ", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {jsonData = pm.response.json();}catch(e){}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", - "};", - "", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", - "};", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", - "} else {", - " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", - "};", - "", - "if (jsonData?.customer_id) {", - " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", - " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", - "} else {", - " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", - "};" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Save card payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount_capturable) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve-copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount_capturable\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", - " function () {", - " pm.expect(jsonData.amount_capturable).to.eql(0);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "", - "// Validate the connector", - "pm.test(\"[POST]::/payments - connector\", function () {", - " pm.expect(jsonData.connector).to.eql(\"stripe\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve Copy", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"connector_transaction_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ - { - "name": "Scenario10-Refund exceeds amount captured", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.amount) {", - " pm.test(", - " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "", - "// Response body should have value \"6000\" for \"amount_received\"", - "if (jsonData?.amount_received) {", - " pm.test(", - " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", - " function () {", - " pm.expect(jsonData.amount_received).to.eql(6000);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"partially_captured\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"The refund amount exceeds the amount captured\" for \"error message\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"The refund amount exceeds the amount captured\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with Invalid card details", - "item": [ - { - "name": "Payments - Create(Invalid card number)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"123456\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp month)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp Year)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "", - "// Response body should have value \"connector error\" for \"error message\"", - "if (jsonData?.error?.message) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", - " function () {", - " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(invalid CVV)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"connector\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"123456\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Confirming the payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Capture greater amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_capture\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_capture\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Capture the succeeded payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Void the success_slash_failure payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Cancel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"cancellation_reason\":\"requested_by_customer\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/cancel", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "cancel" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Create 3DS payment with greater capture", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Refund exceeds amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario8-Refund for unsuccessful payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario9-Create a recurring payment with greater mandate amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have \"mandate_id\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_id' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have \"mandate_data\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'mandate_data' exists\",", - " function () {", - " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Recurring Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - } - ] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "info": { - "_postman_id": "a553df38-fa33-4522-b029-1cd32821730e", - "name": "Stripe Postman Collection", - "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "24206034" - }, - "variable": [ - { - "key": "baseUrl", - "value": "", - "type": "string" - }, - { - "key": "admin_api_key", - "value": "", - "type": "string" - }, - { - "key": "api_key", - "value": "", - "type": "string" - }, - { - "key": "merchant_id", - "value": "" - }, - { - "key": "payment_id", - "value": "" - }, - { - "key": "customer_id", - "value": "" - }, - { - "key": "mandate_id", - "value": "" - }, - { - "key": "payment_method_id", - "value": "" - }, - { - "key": "refund_id", - "value": "" - }, - { - "key": "merchant_connector_id", - "value": "" - }, - { - "key": "client_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_api_key", - "value": "", - "type": "string" - }, - { - "key": "publishable_key", - "value": "", - "type": "string" - }, - { - "key": "api_key_id", - "value": "", - "type": "string" - }, - { - "key": "payment_token", - "value": "" - }, - { - "key": "gateway_merchant_id", - "value": "", - "type": "string" - }, - { - "key": "certificate", - "value": "", - "type": "string" - }, - { - "key": "certificate_keys", - "value": "", - "type": "string" - } - ] -} + "info": { + "_postman_id": "7f70e958-403f-4dc3-ab14-5bc888950601", + "name": "Stripe Postman Collection", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "27028646" + }, + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "MerchantAccounts", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"organization_id\", jsonData.organization_id);", + " console.log(", + " \"- use {{organization_id}} as collection variable for value\",", + " jsonData.organization_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/accounts - Organization id is generated\",", + " function () {", + " pm.expect(typeof jsonData.organization_id !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "Merchant Account - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Retrieve a merchant account details." + }, + "response": [] + }, + { + "name": "Merchant Account - List", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/accounts/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/accounts/list - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/list?organization_id={{organization_id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + "list" + ], + "query": [ + { + "key": "organization_id", + "value": "{{organization_id}}" + } + ], + "variable": [ + { + "key": "organization_id", + "value": "{{organization_id}}", + "description": "(Required) - Organization id" + } + ] + }, + "description": "List merchant accounts for an organization" + }, + "response": [] + }, + { + "name": "Merchant Account - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"{{merchant_id}}\",\"merchant_name\":\"NewAge Retailer\",\"locker_id\":\"m0010\",\"merchant_details\":{\"primary_contact_person\":\"joseph Test\",\"primary_email\":\"josephTest@test.com\",\"primary_phone\":\"veniam aute officia ullamco esse\",\"secondary_contact_person\":\"joseph Test2\",\"secondary_email\":\"josephTest2@test.com\",\"secondary_phone\":\"proident adipisicing officia nulla\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"parent_merchant_id\":\"xkkdf909012sdjki2dkh5sdf\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "To update an existing merchant account. Helpful in updating merchant details such as email, contact deteails, or other configuration details like webhook, routing algorithm etc" + }, + "response": [] + } + ] + }, + { + "name": "API Key", + "item": [ + { + "name": "Create API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Update API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":null,\"description\":\"My very awesome API key\",\"expiration\":null}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Retrieve API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/:api_key_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api_key_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api_key_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api_key_id", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "List API Keys", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/api_keys/:merchant_id/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/api_keys/:merchant_id/list - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + "list" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete API Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/api_keys/:merchant_id/:api-key - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id/:api-key", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id", + ":api-key" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + }, + { + "key": "api-key", + "value": "{{api_key_id}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "PaymentConnectors", + "item": [ + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"first_stripe_connector\")", + " },", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_label\":\"first_stripe_connector\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payment Connector - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/accounts/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}", + "description": "(Required) The unique identifier for the payment connector" + } + ] + }, + "description": "Retrieve Payment Connector details." + }, + "response": [] + }, + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "", + "// Validate if the connector label is the one that is passed in the request", + "pm.test(", + " \"[POST]::/accounts/:account_id/connectors - connector_label is not autogenerated\",", + " function () {", + " pm.expect(jsonData.connector_label).to.eql(\"updated_stripe_connector\")", + " },", + ");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"connector_label\":\"updated_stripe_connector\",\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "List Connectors by MID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "Delete or Detach a Payment Connector from Merchant Account" + }, + "response": [] + }, + { + "name": "Merchant Account - Delete", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[DELETE]::/accounts/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/accounts/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Response Validation", + "const schema = {", + " type: \"object\",", + " description: \"Merchant Account\",", + " required: [\"merchant_id\", \"deleted\"],", + " properties: {", + " merchant_id: {", + " type: \"string\",", + " description: \"The identifier for the MerchantAccount object.\",", + " maxLength: 255,", + " example: \"y3oqhf46pyzuxjbcn2giaqnb44\",", + " },", + " deleted: {", + " type: \"boolean\",", + " description:", + " \"Indicates the deletion status of the Merchant Account object.\",", + " example: true,", + " },", + " },", + "};", + "", + "// Validate if response matches JSON schema", + "pm.test(\"[DELETE]::/accounts/:id - Schema is valid\", function () {", + " pm.response.to.have.jsonSchema(schema, {", + " unknownFormats: [\"int32\", \"int64\", \"float\", \"double\"],", + " });", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/accounts/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Delete a Merchant Account" + }, + "response": [] + } + ] + }, + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "/*", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(\"- use {{api_key}} as collection variable for value\",jsonData.api_key);", + "} else {", + " console.log('INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.');", + "};", + "*/", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}}\n,\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}\n}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have an error message", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' is not 'null'\",", + " function () {", + " pm.expect(jsonData.error_message).is.not.null;", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "Payments - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "", + "// Response body should have \"profile_id\" and not \"null\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'profile_id' exists and is not 'null'\",", + " function () {", + " pm.expect(typeof jsonData.profile_id !== \"undefined\").to.be.true;", + " pm.expect(jsonData.profile_id).is.not.null;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Customers", + "item": [ + { + "name": "Create Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Response body should have \"customer_id\"", + "pm.test(", + " \"[POST]::/customers - Content check if 'customer_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.customer_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have a minimum length of \"1\" for \"customer_id\"", + "if (jsonData?.customer_id) {", + " pm.test(", + " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '1'\",", + " function () {", + " pm.expect(jsonData.customer_id.length).is.at.least(1);", + " },", + " );", + "}", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(", + " \"- use {{customer_id}} as collection variable for value\",", + " jsonData.customer_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"First customer\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/customers", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers" + ] + }, + "description": "Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details." + }, + "response": [] + }, + { + "name": "List Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "", + "// Response body should have a minimum length of \"1\" for \"customer_id\"", + "if (jsonData?.customer_id) {", + " pm.test(", + " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '2'\",", + " function () {", + " pm.expect(jsonData.customer_id.length).is.at.least(2);", + " },", + " );", + "}", + "", + "", + "// Define the regular expression pattern to match customer_id", + "var customerIdPattern = /^[a-zA-Z0-9_]+$/;", + "", + "// Define an array to store the validation results", + "var validationResults = [];", + "", + "// Iterate through the JSON array", + "jsonData.forEach(function(item, index) {", + " if (item.hasOwnProperty(\"customer_id\")) {", + " if (customerIdPattern.test(item.customer_id)) {", + " validationResults.push(\"customer_id \" + item.customer_id + \" is valid.\");", + " } else {", + " validationResults.push(\"customer_id \" + item.customer_id + \" is not valid.\");", + " }", + " } else {", + " validationResults.push(\"customer_id is missing for item at index \" + index);", + " }", + "});", + "", + "// Check if any customer_id is not valid and fail the test if necessary", + "if (validationResults.some(result => !result.includes(\"is valid\"))) {", + " pm.test(\"Customer IDs validation failed: \" + validationResults.join(\", \"), function() {", + " pm.expect(false).to.be.true;", + " });", + "} else {", + " pm.test(\"All customer IDs are valid: \" + validationResults.join(\", \"), function() {", + " pm.expect(true).to.be.true;", + " });", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + "list" + ] + } + }, + "response": [] + }, + { + "name": "Retrieve Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/customers/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/customers/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/customers/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{customer_id}}", + "description": "(Required) unique customer id" + } + ] + }, + "description": "Retrieve a customer's details." + }, + "response": [] + }, + { + "name": "Update Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/customers/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/customers/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/customers/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"email\":\"JohnTest@test.com\",\"name\":\"John Test\",\"phone_country_code\":\"+65\",\"phone\":\"888888888\",\"description\":\"First customer\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/customers/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{customer_id}}", + "description": "(Required) unique customer id" + } + ] + }, + "description": "Updates the customer's details in a customer object." + }, + "response": [] + }, + { + "name": "Ephemeral Key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/ephemeral_keys - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/ephemeral_keys - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"customer_id\":\"{{customer_id}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/ephemeral_keys", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "ephemeral_keys" + ] + } + }, + "response": [] + }, + { + "name": "Delete Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[DELETE]::/customers/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[DELETE]::/customers/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[DELETE]::/customers/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{customer_id}}", + "description": "(Required) unique customer id" + } + ] + }, + "description": "Delete a customer record." + }, + "response": [] + } + ], + "description": "Create a Customer entity which you can use to store and retrieve specific customers' data and payment methods." + }, + { + "name": "Payments", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Session Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/session_tokens - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/session_tokens - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "const responseJson = pm.response.json();", + "", + "// Verify if the wallet_name in the response matches 'apple_pay'", + "pm.test(", + " \"[POST]::/payments/session_tokens - Verify wallet_name is 'apple_pay'\",", + " function () {", + " pm.expect(responseJson.session_token[0].wallet_name).to.eql(\"apple_pay\");", + " },", + ");", + "", + "// Verify if the wallet_name in the response matches 'google_pay'", + "pm.test(", + " \"[POST]::/payments/session_tokens - Verify wallet_name is 'google_pay'\",", + " function () {", + " pm.expect(responseJson.session_token[1].wallet_name).to.eql(\"google_pay\");", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"wallets\":[],\"client_secret\":\"{{client_secret}}\"}" + }, + "url": { + "raw": "{{baseUrl}}/payments/session_tokens", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + "session_tokens" + ] + } + }, + "response": [] + }, + { + "name": "Payments - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":20000,\"currency\":\"SGD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"email\":\"joseph@example.com\",\"name\":\"joseph Doe\",\"phone\":\"8888888888\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"payment_method\":\"card\",\"return_url\":\"https://duck.com\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Confirm (Through Client Secret)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Create Again", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm Again (Through API Key)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Create Yet Again", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payment-List", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/list - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/payments/list - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "api-key", + "value": "snd_0b8e1deb82f241eca47617afb1398858" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/list", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + "list" + ], + "query": [ + { + "key": "customer_id", + "value": "", + "disabled": true + }, + { + "key": "starting_after", + "value": "", + "disabled": true + }, + { + "key": "ending_before", + "value": "", + "disabled": true + }, + { + "key": "limit", + "value": "100", + "disabled": true + }, + { + "key": "created", + "value": "", + "disabled": true + }, + { + "key": "created.lt", + "value": "", + "disabled": true + }, + { + "key": "created.gt", + "value": "", + "disabled": true + }, + { + "key": "created.lte", + "value": "", + "disabled": true + }, + { + "key": "created_gte", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + } + ], + "description": "Process and manage payments across wide range of payment processors using the Unified Payments API." + }, + { + "name": "Refunds", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"reason\":\"Paid by mistake\",\"metadata\":{\"udf1\":\"value2\",\"new_customer\":\"false\",\"login_date\":\"2019-09-1T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To update the properties of a Refund object. This may include attaching a reason for the refund or metadata fields" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "PaymentMethods", + "item": [ + { + "name": "P_Create Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Response body should have \"customer_id\"", + "pm.test(", + " \"[POST]::/customers - Content check if 'customer_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.customer_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have a minimum length of \"1\" for \"customer_id\"", + "if (jsonData?.customer_id) {", + " pm.test(", + " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '1'\",", + " function () {", + " pm.expect(jsonData.customer_id.length).is.at.least(1);", + " },", + " );", + "}", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(", + " \"- use {{customer_id}} as collection variable for value\",", + " jsonData.customer_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"First customer\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/customers", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers" + ] + }, + "description": "Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details." + }, + "response": [] + }, + { + "name": "PaymentMethods - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payment_methods - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payment_methods - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_method_id as variable for jsonData.payment_method_id", + "if (jsonData?.payment_method_id) {", + " pm.collectionVariables.set(\"payment_method_id\", jsonData.payment_method_id);", + " console.log(", + " \"- use {{payment_method_id}} as collection variable for value\",", + " jsonData.payment_method_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_method_id}}, as jsonData.payment_method_id is undefined.\",", + " );", + "}", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(", + " \"- use {{customer_id}} as collection variable for value\",", + " jsonData.customer_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_issuer\":\"Visa\",\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"John Doe\"},\"customer_id\":\"{{customer_id}}\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payment_methods" + ] + }, + "description": "To create a payment method against a customer object. In case of cards, this API could be used only by PCI compliant merchants" + }, + "response": [] + }, + { + "name": "List payment methods for a Merchant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/payment_methods/:merchant_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular merchant id." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(", + " \"- use {{customer_id}} as collection variable for value\",", + " jsonData.customer_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[GET]::/payment_methods/:customer_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[GET]::/payment_methods/:customer_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(", + " \"payment_token\",", + " jsonData.customer_payment_methods[0].payment_token,", + " );", + " console.log(", + " \"- use {{payment_token}} as collection variable for value\",", + " jsonData.customer_payment_methods[0].payment_token,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"},\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}]}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario28-Create PM list for eps dynamic fields", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for Merchants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + " ", + "pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'payment_method_data.bank_redirect.eps.billing_details.billing_name'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"payment_method_data.bank_redirect.eps.billing_details.billing_name\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Scenario2a-Create payment with confirm false card holder name null", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2b-Create payment with confirm false card holder name empty", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create payment with Manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4a-Create payment with manual_multiple capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/cancel - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"cancelled\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'cancelled'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"cancelled\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Create 3DS payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Create 3DS payment with confrm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario8-Create a failure card payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"card_declined\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", + " },", + " );", + "}", + "", + "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000009995\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"card_declined\" for \"error_code\"", + "if (jsonData?.error_code) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_code' matches 'card_declined'\",", + " function () {", + " pm.expect(jsonData.error_code).to.eql(\"card_declined\");", + " },", + " );", + "}", + "", + "// Response body should have value \"message - Your card has insufficient funds., decline_code - insufficient_funds\" for \"error_message\"", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error_message' matches 'message - Your card has insufficient funds., decline_code - insufficient_funds'\",", + " function () {", + " pm.expect(jsonData.error_message).to.eql(\"message - Your card has insufficient funds., decline_code - insufficient_funds\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Refund full payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9a-Partial refund", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"1000\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":1000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '1000'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"refunds\"", + "pm.test(\"[POST]::/payments - Content check if 'refunds' exists\", function () {", + " pm.expect(typeof jsonData.refunds !== \"undefined\").to.be.true;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario10-Create a mandate and recurring payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Recurring Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario11-Refund recurring payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Recurring Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"payment_method_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'payment_method_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.payment_method_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6570,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6570,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario12-BNPL-klarna", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":8000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":8000,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"klarna\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'klarna'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"klarna\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"klarna_redirect\":{\"issuer_name\":\"stripe\",\"billing_email\":\"arjun.karthik@juspay.in\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario13-BNPL-afterpay", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"SE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"SE\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"afterpay_clearpay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'afterpay_clearpay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"afterpay_clearpay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"afterpay_clearpay_redirect\":{\"billing_name\":\"Akshaya\",\"billing_email\":\"example@example.com\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario14-BNPL-affirm", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":7000,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"country\":\"US\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"order_details\":{\"product_name\":\"Socks\",\"amount\":7000,\"quantity\":1}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"affirm\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'affirm'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"affirm\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"pay_later\",\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"payment_method_data\":{\"pay_later\":{\"affirm_redirect\":{\"issuer_name\":\"affirm\",\"billing_email\":\"user-us@example.com\",\"billing_country\":\"US\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario15-Bank Redirect-Ideal", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"ideal\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario16-Bank Redirect-sofort", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"sofort\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_noe_lb_fur_niederosterreich_u_wien\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario17-Bank Redirect-eps", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"eps\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'eps'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"eps\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"eps\",\"payment_method_data\":{\"bank_redirect\":{\"eps\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"hypo_oberosterreich_salzburg_steiermark\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario18-Bank Redirect-giropay", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario19-Bank Transfer-ach", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://hs-payments-test.netlify.app/payments\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.type\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.type' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"ach\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ach'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"ach\");", + " },", + " );", + "}", + "", + "// Response body should have value \"display_bank_transfer_information\" for \"next_action.type\"", + "if (jsonData?.next_action.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'display_bank_transfer_information'\",", + " function () {", + " pm.expect(jsonData.next_action.type).to.eql(", + " \"display_bank_transfer_information\",", + " );", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"bank_transfer\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_transfer\":{\"ach_bank_transfer\":{\"billing_details\":{\"email\":\"johndoe@example.com\"}}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario20-Bank Debit-ach", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1800,\"currency\":\"USD\",\"confirm\":true,\"business_label\":\"default\",\"capture_method\":\"automatic\",\"connector\":[\"stripe\"],\"customer_id\":\"klarna\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"authentication_type\":\"three_ds\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"return_url\":\"https://google.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"setup_future_usage\":\"off_session\",\"business_country\":\"US\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"online\",\"accepted_at\":\"2022-09-10T10:11:12Z\",\"online\":{\"ip_address\":\"123.32.25.123\",\"user_agent\":\"Mozilla/5.0 (Linux; Android 12; SM-S906N Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36\"}},\"mandate_type\":{\"single_use\":{\"amount\":6540,\"currency\":\"USD\"}}},\"payment_method\":\"bank_debit\",\"payment_method_type\":\"ach\",\"payment_method_data\":{\"bank_debit\":{\"ach_bank_debit\":{\"billing_details\":{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"},\"account_number\":\"000123456789\",\"routing_number\":\"110000000\"}}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":1800,\"account_name\":\"transaction_processing\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario21-Wallet-Wechatpay", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":800,\"currency\":\"USD\",\"confirm\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":800,\"customer_id\":\"poll\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"statement_descriptor_name\":\"Juspay\",\"statement_descriptor_suffix\":\"Router\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.type\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.type' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.type !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"ach\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'we_chat_pay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"we_chat_pay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"qr_code_information\" for \"next_action.type\"", + "if (jsonData?.next_action.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'qr_code_information'\",", + " function () {", + " pm.expect(jsonData.next_action.type).to.eql(\"qr_code_information\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"wallet\",\"payment_method_type\":\"we_chat_pay\",\"payment_method_data\":{\"wallet\":{\"we_chat_pay_qr\":{}}},\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario22- Update address and List Payment method", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + "})};", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List Payment Methods for a Merchant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Parse the response body as JSON", + "var responseBody = pm.response.json();", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"card\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"pay_later\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'pay_later'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"pay_later\";", + " });", + "});", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"wallet\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'wallet'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"wallet\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_debit\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_debit'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_debit\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_transfer\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_transfer'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_transfer\";", + " });", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular merchant id." + }, + "response": [] + }, + { + "name": "Payments - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "", + "// Parse the JSON response", + "var jsonData = pm.response.json();", + "", + "// Check if the 'currency' is equal to \"EUR\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'currency' matches 'EUR' \", function () {", + " pm.expect(jsonData.currency).to.eql(\"EUR\");", + "});", + "", + "// Extract the \"country\" field from the JSON data", + "var country = jsonData.billing.address.country;", + "", + "// Check if the country is \"NL\"", + "pm.test(\"[POST]::/payments/:id -Content Check if billing 'Country' matches NL (Netherlands)\", function () {", + " pm.expect(country).to.equal(\"NL\");", + "});", + "", + "var country1 = jsonData.shipping.address.country;", + "", + "// Check if the country is \"NL\"", + "pm.test(\"[POST]::/payments/:id -Content Check if shipping 'Country' matches NL (Netherlands)\", function () {", + " pm.expect(country1).to.equal(\"NL\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"currency\":\"EUR\",\"shipping\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}},\"billing\":{\"address\":{\"line1\":\"1468\",\"line2\":\"Koramangala \",\"line3\":\"Koramangala \",\"city\":\"Bangalore\",\"state\":\"Karnataka\",\"zip\":\"560065\",\"country\":\"NL\",\"first_name\":\"Preeetam\",\"last_name\":\"Rev\"},\"phone\":{\"number\":\"8796455689\",\"country_code\":\"+91\"}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + }, + "response": [] + }, + { + "name": "List Payment Methods for a Merchant-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "", + "// Parse the response body as JSON", + "var responseBody = pm.response.json();", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"card\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'card'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"card\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"ideal\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id - Content Check if payment_method matches 'ideal'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"ideal\";", + " });", + "});", + "", + "// Check if \"payment_methods\" array contains a \"payment_method\" with the value \"bank_redirect\"", + "pm.test(\"[GET]::/payment_methods/:merchant_id -Content Check if payment_method matches 'bank_redirect'\", function () {", + " var paymentMethods = responseBody.payment_methods;", + " var cardPaymentMethod = paymentMethods.find(function (method) {", + " return method.payment_method == \"bank_redirect\";", + " });", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular merchant id." + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "//// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + "})};", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"Example\",\"email\":\"guest@example.com\"},\"bank_name\":\"ing\"}}},\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"125.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"multi_use\":{\"amount\":7000,\"currency\":\"USD\",\"start_date\":\"2023-04-21T00:00:00Z\",\"end_date\":\"2023-05-21T00:00:00Z\",\"metadata\":{\"frequency\":\"13\"}}}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + "})};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario23- Update Amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\", function() {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + "})};", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "", + "// Parse the JSON response", + "var jsonData = pm.response.json();", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "To update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created " + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments/:id/confirm - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "//// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + "})};", + "", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "//// Response body should have value \"amount_received\" for \"1000\"", + "if (jsonData?.amount_received) {", + "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", + " pm.expect(jsonData.amount_received).to.eql(1000);", + "})};", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + }, + { + "key": "publishable_key", + "value": "", + "type": "text", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"setup_future_usage\":\"off_session\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"128.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "//// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + "pm.test(\"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + "})};", + "", + "", + "// Check if the 'amount' is equal to \"1000\"", + "pm.test(\"[POST]::/payments/:id -Content Check if 'amount' matches '1000' \", function () {", + " pm.expect(jsonData.amount).to.eql(1000);", + "});", + "", + "//// Response body should have value \"amount_received\" for \"1000\"", + "if (jsonData?.amount_received) {", + "pm.test(\"[POST]::/payments - Content check if value for 'amount_received' matches '1000'\", function() {", + " pm.expect(jsonData.amount_received).to.eql(1000);", + "})};", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario24-Add card flow", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " })};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4111111111111111\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"737\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"737\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario25-Don't Pass CVV for save card flow and verifysuccess payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"stripesavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"adyen\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(\"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\", function() {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " })};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario26-Save card payment with manual capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"adyensavecard_{{random_number}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"371449635398431\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"7373\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "List payment methods for a Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[GET]::/payment_methods/:customer_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payment_methods/:customer_id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "if (jsonData?.customer_payment_methods[0]?.payment_token) {", + " pm.collectionVariables.set(\"payment_token\", jsonData.customer_payment_methods[0].payment_token);", + " console.log(\"- use {{payment_token}} as collection variable for value\", jsonData.customer_payment_methods[0].payment_token);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_token}}, as jsonData.customer_payment_methods[0].payment_token is undefined.');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/customers/:customer_id/payment_methods", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers", + ":customer_id", + "payment_methods" + ], + "query": [ + { + "key": "accepted_country", + "value": "co", + "disabled": true + }, + { + "key": "accepted_country", + "value": "pa", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "voluptate ea", + "disabled": true + }, + { + "key": "accepted_currency", + "value": "exercitation", + "disabled": true + }, + { + "key": "minimum_amount", + "value": "100", + "disabled": true + }, + { + "key": "maximum_amount", + "value": "10000000", + "disabled": true + }, + { + "key": "recurring_payment_enabled", + "value": "true", + "disabled": true + }, + { + "key": "installment_payment_enabled", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "customer_id", + "value": "{{customer_id}}", + "description": "//Pass the customer id" + } + ] + }, + "description": "To filter and list the applicable payment methods for a particular Customer ID" + }, + "response": [] + }, + { + "name": "Save card payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx ", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");", + "});", + "", + "// Validate if response has JSON Body ", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {jsonData = pm.response.json();}catch(e){}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"- use {{payment_id}} as collection variable for value\",jsonData.payment_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.');", + "};", + "", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(\"- use {{mandate_id}} as collection variable for value\",jsonData.mandate_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.');", + "};", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(\"- use {{client_secret}} as collection variable for value\",jsonData.client_secret);", + "} else {", + " console.log('INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.');", + "};", + "", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(\"- use {{customer_id}} as collection variable for value\",jsonData.customer_id);", + "} else {", + " console.log('INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.');", + "};" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"{{customer_id}}\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://google.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Save card payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'stripe'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"card\",\"payment_token\":\"{{payment_token}}\",\"card_cvc\":\"7373\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount_capturable) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount_capturable\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount_capturable' matches 'amount - 540'\",", + " function () {", + " pm.expect(jsonData.amount_capturable).to.eql(0);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "", + "// Validate the connector", + "pm.test(\"[POST]::/payments - connector\", function () {", + " pm.expect(jsonData.connector).to.eql(\"stripe\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario27-Create payment without customer_id and with billing address and shipping address", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"business_country\":\"US\",\"business_label\":\"default\",\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"bernard123\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_type\":\"debit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"24\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\",\"last_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario10-Refund exceeds amount captured", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]:://payments/:id/capture - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.amount) {", + " pm.test(", + " \"[post]:://payments/:id/capture - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "", + "// Response body should have value \"6000\" for \"amount_received\"", + "if (jsonData?.amount_received) {", + " pm.test(", + " \"[POST]::/payments:id/capture - Content check if value for 'amount_received' matches '6000'\",", + " function () {", + " pm.expect(jsonData.amount_received).to.eql(6000);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":6000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'partially_captured'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"partially_captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"The refund amount exceeds the amount captured\" for \"error message\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.message' matches 'The refund amount exceeds the amount captured'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"The refund amount exceeds the amount captured\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with Invalid card details", + "item": [ + { + "name": "Payments - Create(Invalid card number)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"123456\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"united states\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp month)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"01\",\"card_exp_year\":\"2023\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp Year)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "", + "// Response body should have value \"connector error\" for \"error message\"", + "if (jsonData?.error?.message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'Invalid Expiry Year'\",", + " function () {", + " pm.expect(jsonData.error.message).to.eql(\"Invalid Expiry Year\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(invalid CVV)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"connector\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"123456\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"12345\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Confirming the payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Capture greater amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_capture\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_capture'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_capture\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Capture the succeeded payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Void the success_slash_failure payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Cancel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/cancel - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/cancel - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/cancel - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"cancellation_reason\":\"requested_by_customer\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/cancel", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "cancel" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Create 3DS payment with greater capture", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"manual\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7540,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Refund exceeds amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario8-Refund for unsuccessful payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Create a recurring payment with greater mandate amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"setup_future_usage\":\"off_session\",\"mandate_data\":{\"customer_acceptance\":{\"acceptance_type\":\"offline\",\"accepted_at\":\"1963-05-03T04:07:52.723Z\",\"online\":{\"ip_address\":\"127.0.0.1\",\"user_agent\":\"amet irure esse\"}},\"mandate_type\":{\"single_use\":{\"amount\":7000,\"currency\":\"USD\"}}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"mandate_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have \"mandate_data\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'mandate_data' exists\",", + " function () {", + " pm.expect(typeof jsonData.mandate_data !== \"undefined\").to.be.true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Recurring Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":8040,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"mandate_id\":\"{{mandate_id}}\",\"off_session\":true,\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/postman/collection-json/trustpay.postman_collection.json b/postman/collection-json/trustpay.postman_collection.json index cacc015851d8..f4fa25c371a5 100644 --- a/postman/collection-json/trustpay.postman_collection.json +++ b/postman/collection-json/trustpay.postman_collection.json @@ -1,6420 +1,6737 @@ { - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", - "}", - "", - "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", - "" - ], - "type": "text/javascript" - } - } - ], - "item": [ - { - "name": "Health check", - "item": [ - { - "name": "New Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "x-feature", - "value": "router-custom", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{baseUrl}}/health", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "health" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Flow Testcases", - "item": [ - { - "name": "QuickStart", - "item": [ - { - "name": "Merchant Account - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", - "if (jsonData?.merchant_id) {", - " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", - " console.log(", - " \"- use {{merchant_id}} as collection variable for value\",", - " jsonData.merchant_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", - " );", - "}", - "", - "/*", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(\"- use {{api_key}} as collection variable for value\",jsonData.api_key);", - "} else {", - " console.log('INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.');", - "};", - "*/", - "", - "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", - "if (jsonData?.publishable_key) {", - " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", - " console.log(", - " \"- use {{publishable_key}} as collection variable for value\",", - " jsonData.publishable_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" - }, - "url": { - "raw": "{{baseUrl}}/accounts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "accounts" - ] - }, - "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." - }, - "response": [] - }, - { - "name": "API Key - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", - "if (jsonData?.key_id) {", - " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", - " console.log(", - " \"- use {{api_key_id}} as collection variable for value\",", - " jsonData.key_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", - "if (jsonData?.api_key) {", - " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", - " console.log(", - " \"- use {{api_key}} as collection variable for value\",", - " jsonData.api_key,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" - }, - "url": { - "raw": "{{baseUrl}}/api_keys/:merchant_id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "api_keys", - ":merchant_id" - ], - "variable": [ - { - "key": "merchant_id", - "value": "{{merchant_id}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Payment Connector - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", - " function () {", - " pm.response.to.be.success;", - " },", - ");", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", - "if (jsonData?.merchant_connector_id) {", - " pm.collectionVariables.set(", - " \"merchant_connector_id\",", - " jsonData.merchant_connector_id,", - " );", - " console.log(", - " \"- use {{merchant_connector_id}} as collection variable for value\",", - " jsonData.merchant_connector_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{admin_api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"connector_type\":\"payment_processor\",\"connector_name\":\"trustpay\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}" - }, - "url": { - "raw": "{{baseUrl}}/account/:account_id/connectors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "account", - ":account_id", - "connectors" - ], - "variable": [ - { - "key": "account_id", - "value": "{{merchant_id}}", - "description": "(Required) The unique identifier for the merchant account" - } - ] - }, - "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." - }, - "response": [] - }, - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000001005\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Happy Cases", - "item": [ - { - "name": "Scenario2a-Create payment with confirm false card holder name empty", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2b-Create payment with confirm false card holder name null", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Create payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Create 3DS payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"something\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"65.1.52.138\"},\"return_url\":\"https://integ.hyperswitch.io\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario5-Create 3DS payment with confrm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":65400,\"currency\":\"USD\",\"capture_method\":\"automatic\",\"confirm\":false,\"amount_to_capture\":65400,\"customer_id\":\"hyperswitchCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://github.com/pixincreate\",\"business_country\":\"US\",\"business_label\":\"default\",\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"1234567890\",\"country_code\":\"+1\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"945122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"1234567890\",\"country_code\":\"+1\"}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":65400}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"return_url\":\"https://integ.hyperswitch.io/home\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"John Doe\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"ip_address\":\"65.1.52.138\",\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario6-Refund full payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - }, - { - "name": "Refunds - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "", - "// Response body should have value \"6540\" for \"amount\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", - " function () {", - " pm.expect(jsonData.amount).to.eql(6540);", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/refunds/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{refund_id}}", - "description": "(Required) unique refund id" - } - ] - }, - "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario7-Bank Redirect-Ideal", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"ideal\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'trustpay'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"trustpay\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario8-Bank Redirect-giropay", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"giropay\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'trustpay'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"trustpay\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true", - "disabled": true - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with confirm true", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Create payment with confirm false", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Variation Cases", - "item": [ - { - "name": "Scenario5-Refund for unsuccessful payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_confirmation\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"invalid_request\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario1-Create payment with Invalid card details", - "item": [ - { - "name": "Payments - Create(Invalid card number)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"12345\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid Exp month)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"19\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(Invalid", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Create(invalid CVV)", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - "});", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - } - ] - }, - { - "name": "Scenario2-Confirming the payment without PMD", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"client_secret\":\"{{client_secret}}\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - } - ] - }, - { - "name": "Scenario3-Capture the succeeded payment", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Capture", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", - " function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - " },", - ");", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/capture", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "capture" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To capture the funds for an uncaptured payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario4-Refund exceeds amount", - "item": [ - { - "name": "Payments - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments" - ] - }, - "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Validate if response has JSON Body", - "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", - " pm.response.to.have.jsonBody();", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", - "if (jsonData?.payment_id) {", - " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", - " console.log(", - " \"- use {{payment_id}} as collection variable for value\",", - " jsonData.payment_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", - "if (jsonData?.mandate_id) {", - " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", - " console.log(", - " \"- use {{mandate_id}} as collection variable for value\",", - " jsonData.mandate_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", - " );", - "}", - "", - "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", - "if (jsonData?.client_secret) {", - " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", - " console.log(", - " \"- use {{client_secret}} as collection variable for value\",", - " jsonData.client_secret,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", - " );", - "}", - "", - "// Response body should have value \"Succeeded\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"succeeded\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - }, - { - "name": "Refunds - Create", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 4xx", - "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", - " pm.response.to.be.error;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", - " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", - " \"application/json\",", - " );", - "});", - "", - "// Set response object as internal variable", - "let jsonData = {};", - "try {", - " jsonData = pm.response.json();", - "} catch (e) {}", - "", - "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", - "if (jsonData?.refund_id) {", - " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", - " console.log(", - " \"- use {{refund_id}} as collection variable for value\",", - " jsonData.refund_id,", - " );", - "} else {", - " console.log(", - " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", - " );", - "}", - "", - "// Response body should have \"error\"", - "pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", - " function () {", - " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", - " },", - ");", - "", - "// Response body should have value \"connector error\" for \"error type\"", - "if (jsonData?.error?.type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", - " function () {", - " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "url": { - "raw": "{{baseUrl}}/refunds", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "refunds" - ] - }, - "description": "To create a refund against an already processed payment" - }, - "response": [] - } - ] - } - ] - } - ] - } - ], - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{api_key}}", - "type": "string" - }, - { - "key": "key", - "value": "api-key", - "type": "string" - }, - { - "key": "in", - "value": "header", - "type": "string" - } - ] - }, - "info": { - "_postman_id": "79d4c0d8-454e-45a4-a4ae-749bd9bccb22", - "name": "trustpay", - "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "variable": [ - { - "key": "baseUrl", - "value": "", - "type": "string" - }, - { - "key": "admin_api_key", - "value": "", - "type": "string" - }, - { - "key": "api_key", - "value": "", - "type": "string" - }, - { - "key": "merchant_id", - "value": "" - }, - { - "key": "payment_id", - "value": "" - }, - { - "key": "customer_id", - "value": "" - }, - { - "key": "mandate_id", - "value": "" - }, - { - "key": "payment_method_id", - "value": "" - }, - { - "key": "refund_id", - "value": "" - }, - { - "key": "merchant_connector_id", - "value": "" - }, - { - "key": "client_secret", - "value": "", - "type": "string" - }, - { - "key": "connector_api_key", - "value": "", - "type": "string" - }, - { - "key": "publishable_key", - "value": "", - "type": "string" - }, - { - "key": "api_key_id", - "value": "", - "type": "string" - }, - { - "key": "payment_token", - "value": "" - }, - { - "key": "gateway_merchant_id", - "value": "", - "type": "string" - }, - { - "key": "certificate", - "value": "", - "type": "string" - }, - { - "key": "certificate_keys", - "value": "", - "type": "string" - }, - { - "key": "connector_key1", - "value": "", - "type": "string" - }, - { - "key": "connector_api_secret", - "value": "", - "type": "string" - } - ] -} + "info": { + "_postman_id": "41080d78-8917-4534-81bc-42318eaab2a0", + "name": "trustpay", + "description": "## Get started\n\nJuspay Router provides a collection of APIs that enable you to process and manage payments. Our APIs accept and return JSON in the HTTP body, and return standard HTTP response codes. \nYou can consume the APIs directly using your favorite HTTP/REST library. \nWe have a testing environment referred to \"sandbox\", which you can setup to test API calls without affecting production data.\n\n### Base URLs\n\nUse the following base URLs when making requests to the APIs:\n\n| Environment | Base URL |\n| --- | --- |\n| Sandbox | [https://sandbox.hyperswitch.io](https://sandbox.hyperswitch.io) |\n| Production | [https://router.juspay.io](https://router.juspay.io) |\n\n# Authentication\n\nWhen you sign up for an account, you are given a secret key (also referred as api-key). You may authenticate all API requests with Juspay server by providing the appropriate key in the request Authorization header. \nNever share your secret api keys. Keep them guarded and secure.\n\nContact Support: \nName: Juspay Support \nEmail: [support@juspay.in](mailto:support@juspay.in)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "27028646" + }, + "item": [ + { + "name": "Health check", + "item": [ + { + "name": "New Request", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "x-feature", + "value": "router-custom", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{baseUrl}}/health", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Flow Testcases", + "item": [ + { + "name": "QuickStart", + "item": [ + { + "name": "Merchant Account - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/accounts - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/accounts - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_id as variable for jsonData.merchant_id", + "if (jsonData?.merchant_id) {", + " pm.collectionVariables.set(\"merchant_id\", jsonData.merchant_id);", + " console.log(", + " \"- use {{merchant_id}} as collection variable for value\",", + " jsonData.merchant_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_id}}, as jsonData.merchant_id is undefined.\",", + " );", + "}", + "", + "/*", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(\"- use {{api_key}} as collection variable for value\",jsonData.api_key);", + "} else {", + " console.log('INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.');", + "};", + "*/", + "", + "// pm.collectionVariables - Set publishable_key as variable for jsonData.publishable_key", + "if (jsonData?.publishable_key) {", + " pm.collectionVariables.set(\"publishable_key\", jsonData.publishable_key);", + " console.log(", + " \"- use {{publishable_key}} as collection variable for value\",", + " jsonData.publishable_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{publishable_key}}, as jsonData.publishable_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"merchant_id\":\"postman_merchant_GHAction_{{$guid}}\",\"locker_id\":\"m0010\",\"merchant_name\":\"NewAge Retailer\",\"primary_business_details\":[{\"country\":\"US\",\"business\":\"default\"}],\"merchant_details\":{\"primary_contact_person\":\"John Test\",\"primary_email\":\"JohnTest@test.com\",\"primary_phone\":\"sunt laborum\",\"secondary_contact_person\":\"John Test2\",\"secondary_email\":\"JohnTest2@test.com\",\"secondary_phone\":\"cillum do dolor id\",\"website\":\"www.example.com\",\"about_business\":\"Online Retail with a wide selection of organic products for North America\",\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\"}},\"return_url\":\"https://duck.com/success\",\"webhook_details\":{\"webhook_version\":\"1.0.1\",\"webhook_username\":\"ekart_retail\",\"webhook_password\":\"password_ekart@123\",\"payment_created_enabled\":true,\"payment_succeeded_enabled\":true,\"payment_failed_enabled\":true},\"sub_merchants_enabled\":false,\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/accounts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "accounts" + ] + }, + "description": "Create a new account for a merchant. The merchant could be a seller or retailer or client who likes to receive and send payments." + }, + "response": [] + }, + { + "name": "API Key - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/api_keys/:merchant_id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/api_keys/:merchant_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set api_key_id as variable for jsonData.key_id", + "if (jsonData?.key_id) {", + " pm.collectionVariables.set(\"api_key_id\", jsonData.key_id);", + " console.log(", + " \"- use {{api_key_id}} as collection variable for value\",", + " jsonData.key_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key_id}}, as jsonData.key_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set api_key as variable for jsonData.api_key", + "if (jsonData?.api_key) {", + " pm.collectionVariables.set(\"api_key\", jsonData.api_key);", + " console.log(", + " \"- use {{api_key}} as collection variable for value\",", + " jsonData.api_key,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{api_key}}, as jsonData.api_key is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"name\":\"API Key 1\",\"description\":null,\"expiration\":\"2069-09-23T01:02:03.000Z\"}" + }, + "url": { + "raw": "{{baseUrl}}/api_keys/:merchant_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api_keys", + ":merchant_id" + ], + "variable": [ + { + "key": "merchant_id", + "value": "{{merchant_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Payment Connector - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"connector_type\":\"payment_processor\",\"connector_name\":\"trustpay\",\"connector_account_details\":{\"auth_type\":\"SignatureKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}]}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." + }, + "response": [] + }, + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000001005\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":600,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Happy Cases", + "item": [ + { + "name": "Scenario2a-Create payment with confirm false card holder name empty", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2b-Create payment with confirm false card holder name null", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":null,\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Create payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Create 3DS payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"EUR\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"something\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"65.1.52.138\"},\"return_url\":\"https://integ.hyperswitch.io\",\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario5-Create 3DS payment with confrm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":65400,\"currency\":\"USD\",\"capture_method\":\"automatic\",\"confirm\":false,\"amount_to_capture\":65400,\"customer_id\":\"hyperswitchCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://github.com/pixincreate\",\"business_country\":\"US\",\"business_label\":\"default\",\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"1234567890\",\"country_code\":\"+1\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"945122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"1234567890\",\"country_code\":\"+1\"}},\"metadata\":{\"order_details\":{\"product_name\":\"Apple iphone 15\",\"quantity\":1,\"amount\":65400}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"return_url\":\"https://integ.hyperswitch.io/home\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5200000000000015\",\"card_exp_month\":\"03\",\"card_exp_year\":\"2030\",\"card_holder_name\":\"John Doe\",\"card_cvc\":\"737\",\"card_issuer\":\"\",\"card_network\":\"Visa\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\",\"accept_header\":\"text\\\\/html,application\\\\/xhtml+xml,application\\\\/xml;q=0.9,image\\\\/webp,image\\\\/apng,*\\\\/*;q=0.8\",\"language\":\"en-GB\",\"color_depth\":30,\"ip_address\":\"65.1.52.138\",\"screen_height\":1117,\"screen_width\":1728,\"time_zone\":-330,\"java_enabled\":true,\"java_script_enabled\":true}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario6-Refund full payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/refunds - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":6540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + }, + { + "name": "Refunds - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/refunds/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/refunds/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have value \"6540\" for \"amount\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/refunds - Content check if value for 'amount' matches '6540'\",", + " function () {", + " pm.expect(jsonData.amount).to.eql(6540);", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/refunds/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{refund_id}}", + "description": "(Required) unique refund id" + } + ] + }, + "description": "To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario7-Bank Redirect-Ideal", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"ideal\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'ideal'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"ideal\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'trustpay'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"trustpay\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"ideal\",\"payment_method_data\":{\"bank_redirect\":{\"ideal\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario8-Bank Redirect-giropay", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "", + "// Response body should have value \"giropay\" for \"payment_method_type\"", + "if (jsonData?.payment_method_type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'giropay'\",", + " function () {", + " pm.expect(jsonData.payment_method_type).to.eql(\"giropay\");", + " },", + " );", + "}", + "", + "// Response body should have value \"stripe\" for \"connector\"", + "if (jsonData?.connector) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'trustpay'\",", + " function () {", + " pm.expect(jsonData.connector).to.eql(\"trustpay\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"giropay\",\"payment_method_data\":{\"bank_redirect\":{\"giropay\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"\",\"preferred_language\":\"en\",\"country\":\"DE\"}}}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true", + "disabled": true + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with confirm true", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Create payment with confirm false", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario9-Create PM list for eps dynamic fields", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"AT\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "List payment methods for Merchants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + " ", + "pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'billing.address.first_name'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"billing.address.first_name\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + "pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'billing.address.country'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"billing.address.country\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + "pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'billing.address.zip'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"billing.address.zip\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + "pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'billing.address.city'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"billing.address.city\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + " pm.test(", + " \"[GET]::/payments/:id - EPS required field contains 'billing.address.line1'\",", + " function () {", + " pm.expect(", + " jsonData?.payment_methods &&", + " jsonData.payment_methods.some(", + " (method) =>", + " method.payment_method === \"bank_redirect\" &&", + " method.payment_method_types &&", + " method.payment_method_types.some(", + " (type) =>", + " type.payment_method_type === \"eps\" &&", + " type.required_fields &&", + " type.required_fields[\"billing.address.line1\"]", + " )", + " )", + ").to.eql(true);", + " }", + " );", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/account/payment_methods?client_secret={{client_secret}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + "payment_methods" + ], + "query": [ + { + "key": "client_secret", + "value": "{{client_secret}}" + } + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Variation Cases", + "item": [ + { + "name": "Scenario5-Refund for unsuccessful payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_confirmation\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'requires_confirmation'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_confirmation\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"invalid_request\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":540,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario1-Create payment with Invalid card details", + "item": [ + { + "name": "Payments - Create(Invalid card number)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'connector_error'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"connector_error\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"12345\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid Exp month)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"19\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Create(invalid CVV)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(\"[POST]::/payments - Content check if 'error' exists\", function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + "});", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"12\",\"card_exp_year\":\"2022\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"1234\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + } + ] + }, + { + "name": "Scenario2-Confirming the payment without PMD", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_payment_method\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/confirm - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"client_secret\":\"{{client_secret}}\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, + { + "name": "Scenario3-Capture the succeeded payment", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Capture", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/payments/:id/capture - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/payments/:id/capture - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments/:id/capture - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount_to_capture\":7000,\"statement_descriptor_name\":\"Joseph\",\"statement_descriptor_suffix\":\"JS\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/capture", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "capture" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To capture the funds for an uncaptured payment" + }, + "response": [] + } + ] + }, + { + "name": "Scenario4-Refund exceeds amount", + "item": [ + { + "name": "Payments - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"5100000000000511\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"PiX\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Retrieve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"Succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id" + ], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" + }, + "response": [] + }, + { + "name": "Refunds - Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 4xx", + "pm.test(\"[POST]::/refunds - Status code is 4xx\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/refunds - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set refund_id as variable for jsonData.payment_id", + "if (jsonData?.refund_id) {", + " pm.collectionVariables.set(\"refund_id\", jsonData.refund_id);", + " console.log(", + " \"- use {{refund_id}} as collection variable for value\",", + " jsonData.refund_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{refund_id}}, as jsonData.refund_id is undefined.\",", + " );", + "}", + "", + "// Response body should have \"error\"", + "pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if 'error' exists\",", + " function () {", + " pm.expect(typeof jsonData.error !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have value \"connector error\" for \"error type\"", + "if (jsonData?.error?.type) {", + " pm.test(", + " \"[POST]::/payments/:id/confirm - Content check if value for 'error.type' matches 'invalid_request'\",", + " function () {", + " pm.expect(jsonData.error.type).to.eql(\"invalid_request\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"payment_id\":\"{{payment_id}}\",\"amount\":7000,\"reason\":\"Customer returned product\",\"refund_type\":\"instant\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/refunds", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "refunds" + ] + }, + "description": "To create a refund against an already processed payment" + }, + "response": [] + } + ] + } + ] + } + ] + } + ], + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(\"[LOG]::payment_id - \" + jsonData.payment_id);", + "}", + "", + "console.log(\"[LOG]::x-request-id - \" + pm.response.headers.get(\"x-request-id\"));", + "" + ] + } + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "", + "type": "string" + }, + { + "key": "admin_api_key", + "value": "", + "type": "string" + }, + { + "key": "api_key", + "value": "", + "type": "string" + }, + { + "key": "merchant_id", + "value": "" + }, + { + "key": "payment_id", + "value": "" + }, + { + "key": "customer_id", + "value": "" + }, + { + "key": "mandate_id", + "value": "" + }, + { + "key": "payment_method_id", + "value": "" + }, + { + "key": "refund_id", + "value": "" + }, + { + "key": "merchant_connector_id", + "value": "" + }, + { + "key": "client_secret", + "value": "", + "type": "string" + }, + { + "key": "connector_api_key", + "value": "", + "type": "string" + }, + { + "key": "publishable_key", + "value": "", + "type": "string" + }, + { + "key": "api_key_id", + "value": "", + "type": "string" + }, + { + "key": "payment_token", + "value": "" + }, + { + "key": "gateway_merchant_id", + "value": "", + "type": "string" + }, + { + "key": "certificate", + "value": "", + "type": "string" + }, + { + "key": "certificate_keys", + "value": "", + "type": "string" + }, + { + "key": "connector_key1", + "value": "", + "type": "string" + }, + { + "key": "connector_api_secret", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file From 3cd74966b279dc1c43935dc1bceb1c69b9eb0643 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Mon, 8 Jan 2024 17:40:56 +0530 Subject: [PATCH 286/443] fix(router): Payment link api contract change (#2975) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Kashif Co-authored-by: hrithikeshvm Co-authored-by: hrithikeshvm --- crates/api_models/src/admin.rs | 86 +- crates/api_models/src/payments.rs | 52 +- crates/common_utils/src/consts.rs | 11 +- crates/data_models/src/payments.rs | 1 + .../src/payments/payment_intent.rs | 5 + crates/diesel_models/src/business_profile.rs | 12 + crates/diesel_models/src/payment_intent.rs | 20 +- crates/diesel_models/src/payment_link.rs | 7 +- crates/diesel_models/src/schema.rs | 5 + crates/router/src/consts.rs | 9 + crates/router/src/core/admin.rs | 41 +- crates/router/src/core/payment_link.rs | 197 ++- .../src/core/payment_link/payment_link.html | 1301 ++++++++++++----- .../router/src/core/payment_methods/cards.rs | 60 +- crates/router/src/core/payments.rs | 4 +- crates/router/src/core/payments/helpers.rs | 116 +- .../payments/operations/payment_confirm.rs | 19 +- .../payments/operations/payment_create.rs | 169 ++- .../payments/operations/payment_session.rs | 13 +- .../core/payments/operations/payment_start.rs | 8 - .../payments/operations/payment_status.rs | 12 +- .../payments/operations/payment_update.rs | 20 +- crates/router/src/core/payments/types.rs | 6 +- crates/router/src/core/routing/helpers.rs | 2 + crates/router/src/db/payment_link.rs | 4 +- crates/router/src/macros.rs | 5 +- crates/router/src/openapi.rs | 6 +- crates/router/src/services/api.rs | 2 +- crates/router/src/types/api/admin.rs | 21 +- crates/router/src/types/api/payment_link.rs | 9 +- crates/router/src/types/domain/user.rs | 2 - crates/router/src/types/transformers.rs | 30 +- crates/router/src/utils/user/sample_data.rs | 3 + .../src/mock_db/payment_intent.rs | 1 + .../src/payments/payment_intent.rs | 7 + .../down.sql | 2 + .../up.sql | 3 + .../down.sql | 2 + .../up.sql | 2 + .../down.sql | 2 + .../up.sql | 2 + .../down.sql | 2 + .../up.sql | 2 + openapi/openapi_spec.json | 180 ++- 44 files changed, 1651 insertions(+), 812 deletions(-) create mode 100644 migrations/2023-11-24-112541_add_payment_config_business_profile/down.sql create mode 100644 migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql create mode 100644 migrations/2023-11-24-115538_add_profile_id_payment_link/down.sql create mode 100644 migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql create mode 100644 migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql create mode 100644 migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql create mode 100644 migrations/2023-12-27-104559_business_profile_add_session_expiry/down.sql create mode 100644 migrations/2023-12-27-104559_business_profile_add_session_expiry/up.sql diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index ed49b6f27b5e..c588bb87189f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -95,15 +95,8 @@ pub struct MerchantAccountCreate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - ///Will be used to expire client secret after certain amount of time to be supplied in seconds - ///(900) for 15 mins - #[schema(example = 900)] - pub intent_fulfillment_time: Option, - /// The id of the organization to which the merchant belongs to pub organization_id: Option, - - pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] @@ -185,16 +178,10 @@ pub struct MerchantAccountUpdate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - ///Will be used to expire client secret after certain amount of time to be supplied in seconds - ///(900) for 15 mins - pub intent_fulfillment_time: Option, - /// The default business profile that must be used for creating merchant accounts and payments /// To unset this field, pass an empty string #[schema(max_length = 64)] pub default_profile: Option, - - pub payment_link_config: Option, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -288,8 +275,6 @@ pub struct MerchantAccountResponse { /// A enum value to indicate the status of recon service. By default it is not_requested. #[schema(value_type = ReconStatus, example = "not_requested")] pub recon_status: enums::ReconStatus, - - pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -460,26 +445,6 @@ pub struct PrimaryBusinessDetails { pub business: String, } -#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct PaymentLinkConfig { - #[schema( - max_length = 255, - max_length = 255, - example = "https://i.imgur.com/RfxPFQo.png" - )] - pub merchant_logo: Option, - pub color_scheme: Option, -} - -#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] -#[serde(deny_unknown_fields)] - -pub struct PaymentLinkColorSchema { - pub background_primary_color: Option, - pub sdk_theme: Option, -} - #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct WebhookDetails { @@ -1048,6 +1013,13 @@ pub struct BusinessProfileCreate { /// Verified applepay domains for a particular profile pub applepay_verified_domains: Option>, + + /// Client Secret Default expiry for all payments created under this business profile + #[schema(example = 900)] + pub session_expiry: Option, + + /// Default Payment Link config for all payment links created under this business profile + pub payment_link_config: Option, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -1112,6 +1084,13 @@ pub struct BusinessProfileResponse { /// Verified applepay domains for a particular profile pub applepay_verified_domains: Option>, + + /// Client Secret Default expiry for all payments created under this business profile + #[schema(example = 900)] + pub session_expiry: Option, + + /// Default Payment Link config for all payment links created under this business profile + pub payment_link_config: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -1169,4 +1148,41 @@ pub struct BusinessProfileUpdate { /// Verified applepay domains for a particular profile pub applepay_verified_domains: Option>, + + /// Client Secret Default expiry for all payments created under this business profile + #[schema(example = 900)] + pub session_expiry: Option, + + /// Default Payment Link config for all payment links created under this business profile + pub payment_link_config: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct BusinessPaymentLinkConfig { + pub domain_name: Option, + #[serde(flatten)] + pub config: PaymentLinkConfigRequest, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct PaymentLinkConfigRequest { + /// custom theme for the payment link + #[schema(value_type = Option, max_length = 255, example = "#4E6ADD")] + pub theme: Option, + /// merchant display logo + #[schema(value_type = Option, max_length = 255, example = "https://i.pinimg.com/736x/4d/83/5c/4d835ca8aafbbb15f84d07d926fda473.jpg")] + pub logo: Option, + /// Custom merchant name for payment link + #[schema(value_type = Option, max_length = 255, example = "hyperswitch")] + pub seller_name: Option, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, ToSchema)] +pub struct PaymentLinkConfig { + /// custom theme for the payment link + pub theme: String, + /// merchant display logo + pub logo: String, + /// Custom merchant name for payment link + pub seller_name: String, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 5a894e868a3a..0bfee76304f7 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -296,8 +296,14 @@ pub struct PaymentsRequest { /// additional data that might be required by hyperswitch pub feature_metadata: Option, - /// payment link object required for generating the payment_link - pub payment_link_object: Option, + + /// Whether to get the payment link (if applicable) + #[schema(default = false, example = true)] + pub payment_link: Option, + + /// custom payment link config for the particular payment + #[schema(value_type = Option)] + pub payment_link_config: Option, /// The business profile to use for this payment, if not passed the default business profile /// associated with the merchant account will be used. @@ -314,6 +320,11 @@ pub struct PaymentsRequest { ///Request for an incremental authorization pub request_incremental_authorization: Option, + ///Will be used to expire client secret after certain amount of time to be supplied in seconds + ///(900) for 15 mins + #[schema(example = 900)] + pub session_expiry: Option, + /// additional data related to some frm connectors pub frm_metadata: Option, } @@ -3309,17 +3320,6 @@ mod tests { } } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] -pub struct PaymentLinkObject { - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub link_expiry: Option, - pub merchant_custom_domain_name: Option, - #[schema(value_type = PaymentLinkConfig)] - pub payment_link_config: Option, - /// Custom merchant name for payment link - pub custom_merchant_name: Option, -} - #[derive(Default, Debug, serde::Deserialize, Clone, ToSchema, serde::Serialize)] pub struct RetrievePaymentLinkRequest { pub client_secret: Option, @@ -3339,10 +3339,10 @@ pub struct RetrievePaymentLinkResponse { pub amount: i64, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: PrimitiveDateTime, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] - pub link_expiry: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub expiry: Option, pub description: Option, - pub status: String, + pub status: PaymentLinkStatus, #[schema(value_type = Option)] pub currency: Option, } @@ -3360,14 +3360,15 @@ pub struct PaymentLinkDetails { pub pub_key: String, pub client_secret: String, pub payment_id: String, - #[serde(with = "common_utils::custom_serde::iso8601::option")] - pub expiry: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub session_expiry: PrimitiveDateTime, pub merchant_logo: String, pub return_url: String, pub merchant_name: String, pub order_details: Option>, pub max_items_visible_after_collapse: i8, - pub sdk_theme: Option, + pub theme: String, + pub merchant_description: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3424,6 +3425,13 @@ pub struct PaymentLinkListResponse { pub data: Vec, } +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct PaymentCreatePaymentLinkConfig { + #[serde(flatten)] + #[schema(value_type = Option)] + pub config: admin::PaymentLinkConfigRequest, +} + #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct OrderDetailsWithStringAmount { /// Name of the product that is being purchased @@ -3437,3 +3445,9 @@ pub struct OrderDetailsWithStringAmount { /// Product Image link pub product_img_link: Option, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub enum PaymentLinkStatus { + Active, + Expired, +} diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 7f9533d7eadd..169cb972c066 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -33,11 +33,8 @@ pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; -/// SDK Default Theme const -pub const DEFAULT_SDK_THEME: &str = "#7EA8F6"; - /// Default Payment Link Background color -pub const DEFAULT_BACKGROUND_COLOR: &str = "#E5E5E5"; +pub const DEFAULT_BACKGROUND_COLOR: &str = "#212E46"; /// Default product Img Link pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; @@ -50,3 +47,9 @@ pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/to /// Variable which store the card token for Prophetpay pub const PROPHETPAY_TOKEN: &str = "cctoken"; + +/// Payment intent fulfillment default timeout (in seconds) +pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; + +/// Payment intent default client secret expiry (in seconds) +pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index b3e2c2e520a5..cc6b03f89a5b 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -53,4 +53,5 @@ pub struct PaymentIntent { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub session_expiry: Option, } diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 5389cfdd78de..80671ec7f61d 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -110,6 +110,7 @@ pub struct PaymentIntentNew { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub session_expiry: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -162,6 +163,7 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + session_expiry: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -226,6 +228,7 @@ pub struct PaymentIntentUpdateInternal { pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub session_expiry: Option, } impl From for PaymentIntentUpdateInternal { @@ -249,6 +252,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + session_expiry, } => Self { amount: Some(amount), currency: Some(currency), @@ -268,6 +272,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + session_expiry, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 700104aaaecc..ad66eb7f6f16 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -32,6 +32,8 @@ pub struct BusinessProfile { pub is_recon_enabled: bool, #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, + pub payment_link_config: Option, + pub session_expiry: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -55,6 +57,8 @@ pub struct BusinessProfileNew { pub is_recon_enabled: bool, #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, + pub payment_link_config: Option, + pub session_expiry: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -75,6 +79,8 @@ pub struct BusinessProfileUpdateInternal { pub is_recon_enabled: Option, #[diesel(deserialize_as = super::OptionalDieselArray)] pub applepay_verified_domains: Option>, + pub payment_link_config: Option, + pub session_expiry: Option, } impl From for BusinessProfile { @@ -97,6 +103,8 @@ impl From for BusinessProfile { payout_routing_algorithm: new.payout_routing_algorithm, is_recon_enabled: new.is_recon_enabled, applepay_verified_domains: new.applepay_verified_domains, + payment_link_config: new.payment_link_config, + session_expiry: new.session_expiry, } } } @@ -118,6 +126,8 @@ impl BusinessProfileUpdateInternal { payout_routing_algorithm, is_recon_enabled, applepay_verified_domains, + payment_link_config, + session_expiry, } = self; BusinessProfile { profile_name: profile_name.unwrap_or(source.profile_name), @@ -136,6 +146,8 @@ impl BusinessProfileUpdateInternal { payout_routing_algorithm, is_recon_enabled: is_recon_enabled.unwrap_or(source.is_recon_enabled), applepay_verified_domains, + payment_link_config, + session_expiry, ..source } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index df567f583572..6b546f90787e 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -55,18 +55,11 @@ pub struct PaymentIntent { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub session_expiry: Option, } #[derive( - Clone, - Debug, - Default, - Eq, - PartialEq, - Insertable, - router_derive::DebugAsDisplay, - Serialize, - Deserialize, + Clone, Debug, Eq, PartialEq, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, )] #[diesel(table_name = payment_intent)] pub struct PaymentIntentNew { @@ -112,6 +105,8 @@ pub struct PaymentIntentNew { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub session_expiry: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -164,6 +159,7 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + session_expiry: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -229,6 +225,7 @@ pub struct PaymentIntentUpdateInternal { pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub session_expiry: Option, } impl PaymentIntentUpdate { @@ -261,6 +258,7 @@ impl PaymentIntentUpdate { surcharge_applicable, incremental_authorization_allowed, authorization_count, + session_expiry, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -290,10 +288,10 @@ impl PaymentIntentUpdate { payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), updated_by, surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), - incremental_authorization_allowed: incremental_authorization_allowed .or(source.incremental_authorization_allowed), authorization_count: authorization_count.or(source.authorization_count), + session_expiry, ..source } } @@ -320,6 +318,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + session_expiry, } => Self { amount: Some(amount), currency: Some(currency), @@ -339,6 +338,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + session_expiry, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs index 999a6767d8f3..ed0e979d0268 100644 --- a/crates/diesel_models/src/payment_link.rs +++ b/crates/diesel_models/src/payment_link.rs @@ -18,17 +18,17 @@ pub struct PaymentLink { pub created_at: PrimitiveDateTime, #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, pub payment_link_config: Option, pub description: Option, + pub profile_id: Option, } #[derive( Clone, Debug, - Default, Eq, PartialEq, Insertable, @@ -48,9 +48,10 @@ pub struct PaymentLinkNew { pub created_at: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, - #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + #[serde(with = "common_utils::custom_serde::iso8601::option")] pub fulfilment_time: Option, pub custom_merchant_name: Option, pub payment_link_config: Option, pub description: Option, + pub profile_id: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index f4a0437c6ccb..f4e41cefdef7 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -83,6 +83,8 @@ diesel::table! { payout_routing_algorithm -> Nullable, is_recon_enabled -> Bool, applepay_verified_domains -> Nullable>>, + payment_link_config -> Nullable, + session_expiry -> Nullable, } } @@ -705,6 +707,7 @@ diesel::table! { request_incremental_authorization -> Nullable, incremental_authorization_allowed -> Nullable, authorization_count -> Nullable, + session_expiry -> Nullable, } } @@ -731,6 +734,8 @@ diesel::table! { payment_link_config -> Nullable, #[max_length = 255] description -> Nullable, + #[max_length = 64] + profile_id -> Nullable, } } diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index eff42c0cd7c4..afe761846304 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -24,6 +24,9 @@ pub const REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC: &str = ///Payment intent fulfillment default timeout (in seconds) pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; +/// Payment intent default client secret expiry (in seconds) +pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; + // String literals pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; @@ -71,4 +74,10 @@ pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant"; +/// Max payment session expiry +pub const MAX_SESSION_EXPIRY: u32 = 7890000; + +/// Min payment session expiry +pub const MIN_SESSION_EXPIRY: u32 = 60; + pub const LOCKER_HEALTH_CALL_PATH: &str = "/health"; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 84a2f442de8f..538105932060 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -140,17 +140,6 @@ pub async fn create_merchant_account( .transpose()? .map(Secret::new); - let payment_link_config = req - .payment_link_config - .as_ref() - .map(|pl_metadata| { - utils::Encode::::encode_to_value(pl_metadata) - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config", - }) - }) - .transpose()?; - let organization_id = if let Some(organization_id) = req.organization_id.as_ref() { db.find_organization_by_org_id(organization_id) .await @@ -199,15 +188,15 @@ pub async fn create_merchant_account( primary_business_details, created_at: date_time::now(), modified_at: date_time::now(), + intent_fulfillment_time: None, frm_routing_algorithm: req.frm_routing_algorithm, - intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), payout_routing_algorithm: req.payout_routing_algorithm, id: None, organization_id, is_recon_enabled: false, default_profile: None, recon_status: diesel_models::enums::ReconStatus::NotRequested, - payment_link_config, + payment_link_config: None, }) } .await @@ -429,6 +418,8 @@ pub async fn update_business_profile_cascade( frm_routing_algorithm: None, payout_routing_algorithm: None, applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, }; let update_futures = business_profiles.iter().map(|business_profile| async { @@ -581,10 +572,10 @@ pub async fn merchant_account_update( publishable_key: None, primary_business_details, frm_routing_algorithm: req.frm_routing_algorithm, - intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), + intent_fulfillment_time: None, payout_routing_algorithm: req.payout_routing_algorithm, default_profile: business_profile_id_update, - payment_link_config: req.payment_link_config, + payment_link_config: None, }; let response = db @@ -1426,6 +1417,9 @@ pub async fn create_business_profile( request: api::BusinessProfileCreate, merchant_id: &str, ) -> RouterResponse { + if let Some(session_expiry) = &request.session_expiry { + helpers::validate_session_expiry(session_expiry.to_owned())?; + } let db = state.store.as_ref(); let key_store = db .get_merchant_key_store_by_merchant_id(merchant_id, &db.get_master_key().to_vec().into()) @@ -1539,6 +1533,10 @@ pub async fn update_business_profile( })? } + if let Some(session_expiry) = &request.session_expiry { + helpers::validate_session_expiry(session_expiry.to_owned())?; + } + let webhook_details = request .webhook_details .as_ref() @@ -1561,6 +1559,17 @@ pub async fn update_business_profile( .attach_printable("Invalid routing algorithm given")?; } + let payment_link_config = request + .payment_link_config + .as_ref() + .map(|pl_metadata| { + utils::Encode::::encode_to_value(pl_metadata) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + }) + .transpose()?; + let business_profile_update = storage::business_profile::BusinessProfileUpdateInternal { profile_name: request.profile_name, modified_at: Some(date_time::now()), @@ -1576,6 +1585,8 @@ pub async fn update_business_profile( payout_routing_algorithm: request.payout_routing_algorithm, is_recon_enabled: None, applepay_verified_domains: request.applepay_verified_domains, + payment_link_config, + session_expiry: request.session_expiry.map(i64::from), }; let updated_business_profile = db diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 81b06f5f9aa8..f2043d392ab2 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,7 +1,8 @@ use api_models::admin as admin_types; use common_utils::{ consts::{ - DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, + DEFAULT_SESSION_EXPIRY, }, ext_traits::{OptionExt, ValueExt}, }; @@ -27,15 +28,20 @@ pub async fn retrieve_payment_link( payment_link_id: String, ) -> RouterResponse { let db = &*state.store; - let payment_link_object = db + let payment_link_config = db .find_payment_link_by_payment_link_id(&payment_link_id) .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let status = check_payment_link_status(payment_link_object.fulfilment_time); + let session_expiry = payment_link_config.fulfilment_time.unwrap_or_else(|| { + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) + }); + + let status = check_payment_link_status(session_expiry); let response = api_models::payments::RetrievePaymentLinkResponse::foreign_from(( - payment_link_object, + payment_link_config, status, )); Ok(services::ApplicationResponse::Json(response)) @@ -74,15 +80,25 @@ pub async fn intiate_payment_link_flow( "use payment link for", )?; + let merchant_name_from_merchant_account = merchant_account + .merchant_name + .clone() + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(); + let payment_link = db .find_payment_link_by_payment_link_id(&payment_link_id) .await .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; - let payment_link_config = if let Some(pl_config) = payment_link.payment_link_config.clone() { - extract_payment_link_config(Some(pl_config))? + let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config { + extract_payment_link_config(pl_config_value)? } else { - extract_payment_link_config(merchant_account.payment_link_config.clone())? + admin_types::PaymentLinkConfig { + theme: DEFAULT_BACKGROUND_COLOR.to_string(), + logo: DEFAULT_MERCHANT_LOGO.to_string(), + seller_name: merchant_name_from_merchant_account, + } }; let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { @@ -102,8 +118,13 @@ pub async fn intiate_payment_link_flow( )?; let order_details = validate_order_details(payment_intent.order_details, currency)?; - let (default_sdk_theme, default_background_color) = - (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); + let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { + common_utils::date_time::now() + .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) + }); + + // converting first letter of merchant name to upperCase + let merchant_name = capitalize_first_char(&payment_link_config.seller_name); let payment_details = api_models::payments::PaymentLinkDetails { amount: currency @@ -112,38 +133,20 @@ pub async fn intiate_payment_link_flow( .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?, currency, payment_id: payment_intent.payment_id, - merchant_name: payment_link.custom_merchant_name.unwrap_or( - merchant_account - .merchant_name - .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) - .unwrap_or_default(), - ), + merchant_name, order_details, return_url, - expiry: payment_link.fulfilment_time, + session_expiry, pub_key, client_secret, - merchant_logo: payment_link_config - .clone() - .map(|pl_config| { - pl_config - .merchant_logo - .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()) - }) - .unwrap_or_default(), + merchant_logo: payment_link_config.clone().logo, max_items_visible_after_collapse: 3, - sdk_theme: payment_link_config.clone().and_then(|pl_config| { - pl_config - .color_scheme - .map(|color| color.sdk_theme.unwrap_or(default_sdk_theme.to_string())) - }), + theme: payment_link_config.clone().theme, + merchant_description: payment_intent.description, }; let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css( - payment_link_config.clone(), - default_background_color.to_string(), - ); + let css_script = get_color_scheme_css(payment_link_config.clone()); let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), @@ -168,20 +171,8 @@ fn get_js_script( Ok(format!("window.__PAYMENT_DETAILS = {payment_details_str};")) } -fn get_color_scheme_css( - payment_link_config: Option, - default_primary_color: String, -) -> String { - let background_primary_color = payment_link_config - .and_then(|pl_config| { - pl_config.color_scheme.map(|color| { - color - .background_primary_color - .unwrap_or(default_primary_color.clone()) - }) - }) - .unwrap_or(default_primary_color); - +fn get_color_scheme_css(payment_link_config: api_models::admin::PaymentLinkConfig) -> String { + let background_primary_color = payment_link_config.theme; format!( ":root {{ --primary-color: {background_primary_color}; @@ -226,13 +217,15 @@ pub async fn list_payment_link( Ok(services::ApplicationResponse::Json(payment_link_list)) } -pub fn check_payment_link_status(fulfillment_time: Option) -> String { - let curr_time = Some(common_utils::date_time::now()); +pub fn check_payment_link_status( + max_age: PrimitiveDateTime, +) -> api_models::payments::PaymentLinkStatus { + let curr_time = common_utils::date_time::now(); - if curr_time > fulfillment_time { - "expired".to_string() + if curr_time > max_age { + api_models::payments::PaymentLinkStatus::Expired } else { - "active".to_string() + api_models::payments::PaymentLinkStatus::Active } } @@ -276,7 +269,8 @@ fn validate_order_details( .to_currency_base_unit(order.amount) .into_report() .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; - order_details_amount_string.product_name = order.product_name.clone(); + order_details_amount_string.product_name = + capitalize_first_char(&order.product_name.clone()); order_details_amount_string.quantity = order.quantity; order_details_amount_string_array.push(order_details_amount_string) } @@ -287,16 +281,91 @@ fn validate_order_details( Ok(updated_order_details) } -fn extract_payment_link_config( - pl_config: Option, -) -> Result, error_stack::Report> { - pl_config - .map(|config| { - serde_json::from_value::(config) - .into_report() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config", - }) +pub fn extract_payment_link_config( + pl_config: serde_json::Value, +) -> Result> { + serde_json::from_value::(pl_config.clone()) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", }) - .transpose() +} + +pub fn get_payment_link_config_based_on_priority( + payment_create_link_config: Option, + business_link_config: Option, + merchant_name: String, + default_domain_name: String, +) -> Result<(admin_types::PaymentLinkConfig, String), error_stack::Report> +{ + let (domain_name, business_config) = if let Some(business_config) = business_link_config { + let extracted_value: api_models::admin::BusinessPaymentLinkConfig = business_config + .parse_value("BusinessPaymentLinkConfig") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_link_config", + }) + .attach_printable("Invalid payment_link_config given in business config")?; + + ( + extracted_value + .domain_name + .clone() + .map(|d_name| format!("https://{}", d_name)) + .unwrap_or_else(|| default_domain_name.clone()), + Some(extracted_value.config), + ) + } else { + (default_domain_name, None) + }; + + let theme = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.theme.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.theme.clone()) + }) + .unwrap_or(DEFAULT_BACKGROUND_COLOR.to_string()); + + let logo = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.logo.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.logo.clone()) + }) + .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()); + + let seller_name = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.seller_name.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.seller_name.clone()) + }) + .unwrap_or(merchant_name.clone()); + + let payment_link_config = admin_types::PaymentLinkConfig { + theme, + logo, + seller_name, + }; + + Ok((payment_link_config, domain_name)) +} + +fn capitalize_first_char(s: &str) -> String { + if let Some(first_char) = s.chars().next() { + let capitalized = first_char.to_uppercase(); + let mut result = capitalized.to_string(); + if let Some(remaining) = s.get(1..) { + result.push_str(remaining); + } + result + } else { + s.to_owned() + } } diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 0ca4abd340d6..4fb5bb98efe6 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -1,9 +1,9 @@ - + - {{ hyperloader_sdk_link }} + Payments requested by HyperSwitch + + + + + +
+
+
+
+
+
+ + diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 54df02855120..fdaaa87bf407 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -733,11 +733,17 @@ pub enum ApplicationResponse { TextPlain(String), JsonForRedirection(api::RedirectionResponse), Form(Box), - PaymenkLinkForm(Box), + PaymenkLinkForm(Box), FileData((Vec, mime::Mime)), JsonWithHeaders((R, Vec<(String, String)>)), } +#[derive(Debug, Eq, PartialEq)] +pub enum PaymentLinkAction { + PaymentLinkFormData(PaymentLinkFormData), + PaymentLinkStatus(PaymentLinkStatusData), +} + #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentLinkFormData { pub js_script: String, @@ -745,6 +751,12 @@ pub struct PaymentLinkFormData { pub sdk_url: String, } +#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentLinkStatusData { + pub js_script: String, + pub css_script: String, +} + #[derive(Debug, Eq, PartialEq)] pub struct RedirectionFormData { pub redirect_form: RedirectForm, @@ -1051,16 +1063,32 @@ where .map_into_boxed_body() } - Ok(ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { - match build_payment_link_html(*payment_link_data) { - Ok(rendered_html) => http_response_html_data(rendered_html), - Err(_) => http_response_err( - r#"{ - "error": { - "message": "Error while rendering payment link html page" - } - }"#, - ), + Ok(ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { + match *boxed_payment_link_data { + PaymentLinkAction::PaymentLinkFormData(payment_link_data) => { + match build_payment_link_html(payment_link_data) { + Ok(rendered_html) => http_response_html_data(rendered_html), + Err(_) => http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link html page" + } + }"#, + ), + } + } + PaymentLinkAction::PaymentLinkStatus(payment_link_data) => { + match get_payment_link_status(payment_link_data) { + Ok(rendered_html) => http_response_html_data(rendered_html), + Err(_) => http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link status page" + } + }"#, + ), + } + } } } @@ -1634,6 +1662,26 @@ fn get_hyper_loader_sdk(sdk_url: &str) -> String { format!("") } +pub fn get_payment_link_status( + payment_link_data: PaymentLinkStatusData, +) -> CustomResult { + let html_template = include_str!("../core/payment_link/status.html").to_string(); + let mut tera = Tera::default(); + let _ = tera.add_raw_template("payment_link_status", &html_template); + + let mut context = Context::new(); + context.insert("css_color_scheme", &payment_link_data.css_script); + context.insert("payment_details_js_script", &payment_link_data.js_script); + + match tera.render("payment_link_status", &context) { + Ok(rendered_html) => Ok(rendered_html), + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/router/src/types/api/payment_link.rs b/crates/router/src/types/api/payment_link.rs index d0ce8c043baa..85cb539d4118 100644 --- a/crates/router/src/types/api/payment_link.rs +++ b/crates/router/src/types/api/payment_link.rs @@ -15,7 +15,8 @@ pub(crate) trait PaymentLinkResponseExt: Sized { impl PaymentLinkResponseExt for RetrievePaymentLinkResponse { async fn from_db_payment_link(payment_link: storage::PaymentLink) -> RouterResult { let session_expiry = payment_link.fulfilment_time.unwrap_or_else(|| { - common_utils::date_time::now() + payment_link + .created_at .saturating_add(time::Duration::seconds(DEFAULT_SESSION_EXPIRY)) }); let status = payment_link::check_payment_link_status(session_expiry); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index bf0df4dc4b27..4e6c69b2ebd7 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8760,8 +8760,8 @@ "PaymentLinkStatus": { "type": "string", "enum": [ - "Active", - "Expired" + "active", + "expired" ] }, "PaymentListConstraints": { From 8830563748ed20c40b7a21a66e9ad9fd02ddcf0e Mon Sep 17 00:00:00 2001 From: Jeeva Ramachandran <120017870+JeevaRamu0104@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:56:00 +0530 Subject: [PATCH 304/443] fix(euclid_wasm): Update braintree config prod (#3288) --- crates/connector_configs/toml/production.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 38a41b40f7a7..cbc2bb238021 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -359,6 +359,9 @@ key1="Merchant Id" api_secret="Private Key" [braintree.connector_webhook_details] merchant_secret="Source verification key" +[braintree.metadata] +merchant_account_id="Merchant Account Id" +merchant_config_currency="Currency" [bambora] [[bambora.credit]] From 612f8d9d5f5bcba78aa64c3128cc72be0f2860ea Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Wed, 10 Jan 2024 15:51:50 +0530 Subject: [PATCH 305/443] refactor: removed basilisk feature (#3281) Co-authored-by: venkatesh.devendran --- config/config.example.toml | 6 - config/development.toml | 6 - config/docker_compose.toml | 6 - crates/router/Cargo.toml | 3 +- crates/router/src/configs/kms.rs | 8 - crates/router/src/configs/settings.rs | 6 - .../router/src/core/payment_methods/vault.rs | 264 ------------------ crates/router/src/services/api/client.rs | 5 - loadtest/config/development.toml | 6 - 9 files changed, 1 insertion(+), 309 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 4cb2bc085bc9..7e32b2f5d3b1 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -139,12 +139,6 @@ connectors_with_delayed_session_response = "trustpay,payme" # List of connectors connectors_with_webhook_source_verification_call = "paypal" # List of connectors which has additional source verification api-call [jwekey] # 4 priv/pub key pair -locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk -locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk -locker_encryption_key1 = "" # public key 1 in pem format, corresponding private key in basilisk -locker_encryption_key2 = "" # public key 2 in pem format, corresponding private key in basilisk -locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk -locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs diff --git a/config/development.toml b/config/development.toml index 23917cec3aa7..ebd4cb1c93e6 100644 --- a/config/development.toml +++ b/config/development.toml @@ -80,12 +80,6 @@ fallback_api_key = "YOUR API KEY HERE" redis_lock_timeout = 26000 [jwekey] -locker_key_identifier1 = "" -locker_key_identifier2 = "" -locker_encryption_key1 = "" -locker_encryption_key2 = "" -locker_decryption_key1 = "" -locker_decryption_key2 = "" vault_encryption_key = "" rust_locker_encryption_key = "" vault_private_key = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 59aaba2e5098..a8cf5bfb0519 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -58,12 +58,6 @@ mock_locker = true basilisk_host = "" [jwekey] -locker_key_identifier1 = "" -locker_key_identifier2 = "" -locker_encryption_key1 = "" -locker_encryption_key2 = "" -locker_decryption_key1 = "" -locker_decryption_key2 = "" vault_encryption_key = "" rust_locker_encryption_key = "" vault_private_key = "" diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index eb7fbc7ddbc9..8ecac3620919 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -14,9 +14,8 @@ s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] -basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen"] +release = ["kms", "stripe", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index bf6ee44d28be..4e236a512acf 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -13,19 +13,11 @@ impl KmsDecrypt for settings::Jwekey { kms_client: &KmsClient, ) -> CustomResult { ( - self.locker_encryption_key1, - self.locker_encryption_key2, - self.locker_decryption_key1, - self.locker_decryption_key2, self.vault_encryption_key, self.rust_locker_encryption_key, self.vault_private_key, self.tunnel_private_key, ) = tokio::try_join!( - kms_client.decrypt(self.locker_encryption_key1), - kms_client.decrypt(self.locker_encryption_key2), - kms_client.decrypt(self.locker_decryption_key1), - kms_client.decrypt(self.locker_decryption_key2), kms_client.decrypt(self.vault_encryption_key), kms_client.decrypt(self.rust_locker_encryption_key), kms_client.decrypt(self.vault_private_key), diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index db59d7f29148..b7aa3d3ea5dd 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -499,12 +499,6 @@ pub struct EphemeralConfig { #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct Jwekey { - pub locker_key_identifier1: String, - pub locker_key_identifier2: String, - pub locker_encryption_key1: String, - pub locker_encryption_key2: String, - pub locker_decryption_key1: String, - pub locker_decryption_key2: String, pub vault_encryption_key: String, pub rust_locker_encryption_key: String, pub vault_private_key: String, diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index c71632c9b06d..c25b0241581d 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -4,8 +4,6 @@ use common_utils::{ generate_id_with_default_len, }; use error_stack::{report, IntoReport, ResultExt}; -#[cfg(feature = "basilisk")] -use josekit::jwe; use masking::PeekInterface; use router_env::{instrument, tracing}; use scheduler::{types::process_data, utils as process_tracker_utils}; @@ -23,11 +21,7 @@ use crate::{ }, utils::{self, StringExt}, }; -#[cfg(feature = "basilisk")] -use crate::{core::payment_methods::transformers as payment_methods, services, settings}; const VAULT_SERVICE_NAME: &str = "CARD"; -#[cfg(feature = "basilisk")] -const VAULT_VERSION: &str = "0"; pub struct SupplementaryVaultData { pub customer_id: Option, @@ -806,11 +800,6 @@ pub async fn create_tokenize( } Err(err) => { logger::error!("Redis Temp locker Failed: {:?}", err); - - #[cfg(feature = "basilisk")] - return old_create_tokenize(state, value1, value2, lookup_key).await; - - #[cfg(not(feature = "basilisk"))] Err(err) } } @@ -874,11 +863,6 @@ pub async fn get_tokenized_data( } Err(err) => { logger::error!("Redis Temp locker Failed: {:?}", err); - - #[cfg(feature = "basilisk")] - return old_get_tokenized_data(state, lookup_key, _should_get_value2).await; - - #[cfg(not(feature = "basilisk"))] Err(err) } } @@ -925,11 +909,6 @@ pub async fn delete_tokenized_data(state: &routes::AppState, lookup_key: &str) - } Err(err) => { logger::error!("Redis Temp locker Failed: {:?}", err); - - #[cfg(feature = "basilisk")] - return old_delete_tokenized_data(state, lookup_key).await; - - #[cfg(not(feature = "basilisk"))] Err(err) } } @@ -1056,246 +1035,3 @@ pub async fn retry_delete_tokenize( } // Fallback logic of old temp locker needs to be removed later - -#[cfg(feature = "basilisk")] -async fn get_locker_jwe_keys( - keys: &settings::ActiveKmsSecrets, -) -> CustomResult<(String, String), errors::EncryptionError> { - let keys = keys.jwekey.peek(); - let key_id = get_key_id(keys); - let (public_key, private_key) = if key_id == keys.locker_key_identifier1 { - (&keys.locker_encryption_key1, &keys.locker_decryption_key1) - } else if key_id == keys.locker_key_identifier2 { - (&keys.locker_encryption_key2, &keys.locker_decryption_key2) - } else { - return Err(errors::EncryptionError.into()); - }; - - Ok((public_key.to_string(), private_key.to_string())) -} - -#[cfg(feature = "basilisk")] -#[instrument(skip(state, value1, value2))] -pub async fn old_create_tokenize( - state: &routes::AppState, - value1: String, - value2: Option, - lookup_key: String, -) -> RouterResult { - let payload_to_be_encrypted = api::TokenizePayloadRequest { - value1, - value2: value2.unwrap_or_default(), - lookup_key, - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = utils::Encode::::encode_to_string_of_json( - &payload_to_be_encrypted, - ) - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - let (public_key, private_key) = get_locker_jwe_keys(&state.kms_secrets) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some(VAULT_VERSION.to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making tokenize request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - - match response { - Ok(r) => { - let resp: api::TokenizePayloadEncrypted = r - .response - .parse_struct("TokenizePayloadEncrypted") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; - let alg = jwe::RSA_OAEP_256; - let decrypted_payload = services::decrypt_jwe( - &resp.payload, - services::KeyIdCheck::RequestResponseKeyId(( - get_key_id(&state.conf.jwekey), - &resp.key_id, - )), - private_key, - alg, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decrypt Jwe failed for TokenizePayloadEncrypted")?; - let get_response: api::GetTokenizePayloadResponse = decrypted_payload - .parse_struct("GetTokenizePayloadResponse") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "Error getting GetTokenizePayloadResponse from tokenize response", - )?; - Ok(get_response.lookup_key) - } - Err(err) => { - metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got 4xx from the basilisk locker: {err:?}")) - } - } -} - -#[cfg(feature = "basilisk")] -pub async fn old_get_tokenized_data( - state: &routes::AppState, - lookup_key: &str, - should_get_value2: bool, -) -> RouterResult { - metrics::GET_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); - let payload_to_be_encrypted = api::GetTokenizePayloadRequest { - lookup_key: lookup_key.to_string(), - get_value2: should_get_value2, - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = serde_json::to_string(&payload_to_be_encrypted) - .map_err(|_x| errors::ApiErrorResponse::InternalServerError)?; - - let (public_key, private_key) = get_locker_jwe_keys(&state.kms_secrets) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some("0".to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize/get", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making Get Tokenized request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError)?; - match response { - Ok(r) => { - let resp: api::TokenizePayloadEncrypted = r - .response - .parse_struct("TokenizePayloadEncrypted") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for TokenizePayloadEncrypted")?; - let alg = jwe::RSA_OAEP_256; - let decrypted_payload = services::decrypt_jwe( - &resp.payload, - services::KeyIdCheck::RequestResponseKeyId(( - get_key_id(&state.conf.jwekey), - &resp.key_id, - )), - private_key, - alg, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("GetTokenizedApi: Decrypt Jwe failed for TokenizePayloadEncrypted")?; - let get_response: api::TokenizePayloadRequest = decrypted_payload - .parse_struct("TokenizePayloadRequest") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting TokenizePayloadRequest from tokenize response")?; - Ok(get_response) - } - Err(err) => { - metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - match err.status_code { - 404 => Err(errors::ApiErrorResponse::UnprocessableEntity { - message: "Token is invalid or expired".into(), - } - .into()), - _ => Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got error from the basilisk locker: {err:?}")), - } - } - } -} - -#[cfg(feature = "basilisk")] -pub async fn old_delete_tokenized_data( - state: &routes::AppState, - lookup_key: &str, -) -> RouterResult<()> { - metrics::DELETED_TOKENIZED_CARD.add(&metrics::CONTEXT, 1, &[]); - let payload_to_be_encrypted = api::DeleteTokenizeByTokenRequest { - lookup_key: lookup_key.to_string(), - service_name: VAULT_SERVICE_NAME.to_string(), - }; - let payload = serde_json::to_string(&payload_to_be_encrypted) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error serializing api::DeleteTokenizeByTokenRequest")?; - - let (public_key, _private_key) = get_locker_jwe_keys(&state.kms_secrets.clone()) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encryption key")?; - let encrypted_payload = services::encrypt_jwe(payload.as_bytes(), public_key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting Encrypt JWE response")?; - let create_tokenize_request = api::TokenizePayloadEncrypted { - payload: encrypted_payload, - key_id: get_key_id(&state.conf.jwekey).to_string(), - version: Some("0".to_string()), - }; - let request = payment_methods::mk_crud_locker_request( - &state.conf.locker, - "/tokenize/delete/token", - create_tokenize_request, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Making Delete Tokenized request failed")?; - let response = services::call_connector_api(state, request) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error while making /tokenize/delete/token call to the locker")?; - match response { - Ok(r) => { - let _delete_response = std::str::from_utf8(&r.response) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Decoding Failed for basilisk delete response")?; - Ok(()) - } - Err(err) => { - metrics::TEMP_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); - Err(errors::ApiErrorResponse::InternalServerError) - .into_report() - .attach_printable(format!("Got 4xx from the basilisk locker: {err:?}")) - } - } -} - -#[cfg(feature = "basilisk")] -pub fn get_key_id(keys: &settings::Jwekey) -> &str { - let key_identifier = "1"; // [#46]: Fetch this value from redis or external sources - if key_identifier == "1" { - &keys.locker_key_identifier1 - } else { - &keys.locker_key_identifier2 - } -} diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index fca85c41699a..f4d74c4f81bb 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -112,7 +112,6 @@ pub(super) fn create_client( pub fn proxy_bypass_urls(locker: &Locker) -> Vec { let locker_host = locker.host.to_owned(); let locker_host_rs = locker.host_rs.to_owned(); - let basilisk_host = locker.basilisk_host.to_owned(); vec![ format!("{locker_host}/cards/add"), format!("{locker_host}/cards/retrieve"), @@ -124,10 +123,6 @@ pub fn proxy_bypass_urls(locker: &Locker) -> Vec { format!("{locker_host}/card/addCard"), format!("{locker_host}/card/getCard"), format!("{locker_host}/card/deleteCard"), - format!("{basilisk_host}/tokenize"), - format!("{basilisk_host}/tokenize/get"), - format!("{basilisk_host}/tokenize/delete"), - format!("{basilisk_host}/tokenize/delete/token"), ] } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index e1f94c4f80a3..066933317b02 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -51,12 +51,6 @@ max_attempts = 10 max_age = 365 [jwekey] -locker_key_identifier1 = "" -locker_key_identifier2 = "" -locker_encryption_key1 = "" -locker_encryption_key2 = "" -locker_decryption_key1 = "" -locker_decryption_key2 = "" vault_encryption_key = "" rust_locker_encryption_key = "" vault_private_key = "" From fe3cf54781302c733c1682ded2c1735544407a5f Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Wed, 10 Jan 2024 12:27:53 +0100 Subject: [PATCH 306/443] chore: nits and small code improvements found during investigation of PR#3168 (#3259) --- crates/router/src/connector/utils.rs | 8 ++++---- crates/router/src/connector/worldline/transformers.rs | 2 +- crates/router/src/core/fraud_check.rs | 7 +++---- crates/router/src/core/payment_methods/vault.rs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 886de4174db4..39b404d0f558 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -806,7 +806,7 @@ impl CardData for api::Card { let year = self.get_card_expiry_year_2_digit()?; Ok(Secret::new(format!( "{}{}{}", - self.card_exp_month.peek().clone(), + self.card_exp_month.peek(), delimiter, year.peek() ))) @@ -817,14 +817,14 @@ impl CardData for api::Card { "{}{}{}", year.peek(), delimiter, - self.card_exp_month.peek().clone() + self.card_exp_month.peek() )) } fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret { let year = self.get_expiry_year_4_digit(); Secret::new(format!( "{}{}{}", - self.card_exp_month.peek().clone(), + self.card_exp_month.peek(), delimiter, year.peek() )) @@ -1211,7 +1211,7 @@ where { let connector_meta_secret = connector_meta.ok_or_else(missing_field_err("connector_meta_data"))?; - let json = connector_meta_secret.peek().clone(); + let json = connector_meta_secret.expose(); json.parse_value(std::any::type_name::()).switch() } diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index c00913aa57d1..c55663d59f48 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -342,7 +342,7 @@ fn make_card_request( req: &PaymentsAuthorizeData, ccard: &payments::Card, ) -> Result> { - let expiry_year = ccard.card_exp_year.peek().clone(); + let expiry_year = ccard.card_exp_year.peek(); let secret_value = format!( "{}{}", ccard.card_exp_month.peek(), diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 8be1876aed57..ad3a7638774e 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use api_models::{admin::FrmConfigs, enums as api_enums, payments::AdditionalPaymentData}; use error_stack::ResultExt; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface}; use router_env::{ logger, tracing::{self, instrument}, @@ -167,10 +167,9 @@ where match frm_configs_option { Some(frm_configs_value) => { let frm_configs_struct: Vec = frm_configs_value - .iter() + .into_iter() .map(|config| { config - .peek() - .clone() + .expose() .parse_value("FrmConfigs") .change_context(errors::ApiErrorResponse::InvalidDataFormat { field_name: "frm_configs".to_string(), diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index c25b0241581d..070bca234c8e 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -47,7 +47,7 @@ impl Vaultable for api::Card { exp_month: self.card_exp_month.peek().clone(), name_on_card: self .card_holder_name - .clone() + .as_ref() .map(|name| name.peek().clone()), nickname: None, card_last_four: None, From e0e28b87c0647252918ef110cd7614c46b5cf943 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:58:22 +0530 Subject: [PATCH 307/443] feat(core): add new payments webhook events (#3212) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: hrithikeshvm --- crates/api_models/src/webhooks.rs | 14 +++++++++++++- crates/common_enums/src/enums.rs | 4 ++++ .../router/src/compatibility/stripe/webhooks.rs | 7 +++++++ crates/router/src/connector/nmi.rs | 15 +++++++++++++++ crates/router/src/connector/nmi/transformers.rs | 16 +++++++--------- crates/router/src/connector/stripe.rs | 4 +++- .../router/src/connector/stripe/transformers.rs | 2 +- crates/router/src/types/transformers.rs | 12 ++++++++---- .../down.sql | 2 ++ .../up.sql | 3 +++ openapi/openapi_spec.json | 2 ++ 11 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 migrations/2023-12-28-063619_add_enum_types_to_EventType/down.sql create mode 100644 migrations/2023-12-28-063619_add_enum_types_to_EventType/up.sql diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index bc8e75f6d479..7b3564732bf9 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -8,11 +8,18 @@ use crate::{disputes, enums as api_enums, mandates, payments, refunds}; #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)] #[serde(rename_all = "snake_case")] pub enum IncomingWebhookEvent { + /// Authorization + Capture success PaymentIntentFailure, + /// Authorization + Capture failure PaymentIntentSuccess, PaymentIntentProcessing, PaymentIntentPartiallyFunded, PaymentIntentCancelled, + PaymentIntentCancelFailure, + PaymentIntentAuthorizationSuccess, + PaymentIntentAuthorizationFailure, + PaymentIntentCaptureSuccess, + PaymentIntentCaptureFailure, PaymentActionRequired, EventNotSupported, SourceChargeable, @@ -86,7 +93,12 @@ impl From for WebhookFlow { | IncomingWebhookEvent::PaymentIntentProcessing | IncomingWebhookEvent::PaymentActionRequired | IncomingWebhookEvent::PaymentIntentPartiallyFunded - | IncomingWebhookEvent::PaymentIntentCancelled => Self::Payment, + | IncomingWebhookEvent::PaymentIntentCancelled + | IncomingWebhookEvent::PaymentIntentCancelFailure + | IncomingWebhookEvent::PaymentIntentAuthorizationSuccess + | IncomingWebhookEvent::PaymentIntentAuthorizationFailure + | IncomingWebhookEvent::PaymentIntentCaptureSuccess + | IncomingWebhookEvent::PaymentIntentCaptureFailure => Self::Payment, IncomingWebhookEvent::EventNotSupported => Self::ReturnResponse, IncomingWebhookEvent::RefundSuccess | IncomingWebhookEvent::RefundFailure => { Self::Refund diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 0c4b9720cab8..3af1c0e826be 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -921,10 +921,14 @@ impl Currency { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum EventType { + /// Authorize + Capture success PaymentSucceeded, + /// Authorize + Capture failed PaymentFailed, PaymentProcessing, PaymentCancelled, + PaymentAuthorized, + PaymentCaptured, ActionRequired, RefundSucceeded, RefundFailed, diff --git a/crates/router/src/compatibility/stripe/webhooks.rs b/crates/router/src/compatibility/stripe/webhooks.rs index c44e265a9657..807278e0aff2 100644 --- a/crates/router/src/compatibility/stripe/webhooks.rs +++ b/crates/router/src/compatibility/stripe/webhooks.rs @@ -183,6 +183,13 @@ fn get_stripe_event_type(event_type: api_models::enums::EventType) -> &'static s api_models::enums::EventType::DisputeLost => "dispute.lost", api_models::enums::EventType::MandateActive => "mandate.active", api_models::enums::EventType::MandateRevoked => "mandate.revoked", + + // as per this doc https://stripe.com/docs/api/events/types#event_types-payment_intent.amount_capturable_updated + api_models::enums::EventType::PaymentAuthorized => { + "payment_intent.amount_capturable_updated" + } + // stripe treats partially captured payments as succeeded. + api_models::enums::EventType::PaymentCaptured => "payment_intent.succeeded", } } diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index d514eefb10aa..0550908649ff 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -873,6 +873,21 @@ impl api::IncomingWebhook for Nmi { reference_body.event_body.order_id, ), ), + nmi::NmiActionType::Auth => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Capture => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), + nmi::NmiActionType::Void => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + reference_body.event_body.order_id, + ), + ), nmi::NmiActionType::Refund => api_models::webhooks::ObjectReferenceId::RefundId( api_models::webhooks::RefundIdType::RefundId(reference_body.event_body.order_id), ), diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 677bf303d95f..fcf35bfbe370 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -1166,21 +1166,19 @@ pub enum NmiWebhookEventType { impl ForeignFrom for webhooks::IncomingWebhookEvent { fn foreign_from(status: NmiWebhookEventType) -> Self { match status { - NmiWebhookEventType::SaleSuccess | NmiWebhookEventType::CaptureSuccess => { - Self::PaymentIntentSuccess - } - NmiWebhookEventType::SaleFailure | NmiWebhookEventType::CaptureFailure => { - Self::PaymentIntentFailure - } + NmiWebhookEventType::SaleSuccess => Self::PaymentIntentSuccess, + NmiWebhookEventType::SaleFailure => Self::PaymentIntentFailure, NmiWebhookEventType::RefundSuccess => Self::RefundSuccess, NmiWebhookEventType::RefundFailure => Self::RefundFailure, NmiWebhookEventType::VoidSuccess => Self::PaymentIntentCancelled, + NmiWebhookEventType::AuthSuccess => Self::PaymentIntentAuthorizationSuccess, + NmiWebhookEventType::CaptureSuccess => Self::PaymentIntentCaptureSuccess, + NmiWebhookEventType::AuthFailure => Self::PaymentIntentAuthorizationFailure, + NmiWebhookEventType::CaptureFailure => Self::PaymentIntentCaptureFailure, + NmiWebhookEventType::VoidFailure => Self::PaymentIntentCancelFailure, NmiWebhookEventType::SaleUnknown | NmiWebhookEventType::RefundUnknown - | NmiWebhookEventType::AuthSuccess - | NmiWebhookEventType::AuthFailure | NmiWebhookEventType::AuthUnknown - | NmiWebhookEventType::VoidFailure | NmiWebhookEventType::VoidUnknown | NmiWebhookEventType::CaptureUnknown => Self::EventNotSupported, } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 8c43e2c16a25..c151c5af455a 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1977,6 +1977,9 @@ impl api::IncomingWebhook for Stripe { stripe::WebhookEventType::PaymentIntentCanceled => { api::IncomingWebhookEvent::PaymentIntentCancelled } + stripe::WebhookEventType::PaymentIntentAmountCapturableUpdated => { + api::IncomingWebhookEvent::PaymentIntentAuthorizationSuccess + } stripe::WebhookEventType::ChargeSucceeded => { if let Some(stripe::WebhookPaymentMethodDetails { payment_method: @@ -2033,7 +2036,6 @@ impl api::IncomingWebhook for Stripe { | stripe::WebhookEventType::ChargeRefunded | stripe::WebhookEventType::PaymentIntentCreated | stripe::WebhookEventType::PaymentIntentProcessing - | stripe::WebhookEventType::PaymentIntentAmountCapturableUpdated | stripe::WebhookEventType::SourceTransactionCreated => { api::IncomingWebhookEvent::EventNotSupported } diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 8875fdecfd08..89e186924142 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -3315,7 +3315,7 @@ pub enum WebhookEventType { PaymentIntentProcessing, #[serde(rename = "payment_intent.requires_action")] PaymentIntentRequiresAction, - #[serde(rename = "amount_capturable_updated")] + #[serde(rename = "payment_intent.amount_capturable_updated")] PaymentIntentAmountCapturableUpdated, #[serde(rename = "source.chargeable")] SourceChargeable, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index c3818caf051a..786a8c551824 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -352,11 +352,15 @@ impl ForeignFrom for Option { Some(storage_enums::EventType::ActionRequired) } api_enums::IntentStatus::Cancelled => Some(storage_enums::EventType::PaymentCancelled), + api_enums::IntentStatus::PartiallyCaptured + | api_enums::IntentStatus::PartiallyCapturedAndCapturable => { + Some(storage_enums::EventType::PaymentCaptured) + } + api_enums::IntentStatus::RequiresCapture => { + Some(storage_enums::EventType::PaymentAuthorized) + } api_enums::IntentStatus::RequiresPaymentMethod - | api_enums::IntentStatus::RequiresConfirmation - | api_enums::IntentStatus::RequiresCapture - | api_enums::IntentStatus::PartiallyCaptured - | api_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + | api_enums::IntentStatus::RequiresConfirmation => None, } } } diff --git a/migrations/2023-12-28-063619_add_enum_types_to_EventType/down.sql b/migrations/2023-12-28-063619_add_enum_types_to_EventType/down.sql new file mode 100644 index 000000000000..c7c9cbeb4017 --- /dev/null +++ b/migrations/2023-12-28-063619_add_enum_types_to_EventType/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2023-12-28-063619_add_enum_types_to_EventType/up.sql b/migrations/2023-12-28-063619_add_enum_types_to_EventType/up.sql new file mode 100644 index 000000000000..74b87199c2fd --- /dev/null +++ b/migrations/2023-12-28-063619_add_enum_types_to_EventType/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "EventType" ADD VALUE IF NOT EXISTS 'payment_authorized'; +ALTER TYPE "EventType" ADD VALUE IF NOT EXISTS 'payment_captured'; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4e6c69b2ebd7..df5b9448971d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5556,6 +5556,8 @@ "payment_failed", "payment_processing", "payment_cancelled", + "payment_authorized", + "payment_captured", "action_required", "refund_succeeded", "refund_failed", From a69e876f8212cb94202686e073005c23b1b2fc35 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:04:00 +0530 Subject: [PATCH 308/443] refactor(connector): [bluesnap] add connector_txn_id fallback for webhook (#3315) --- crates/router/src/connector/bluesnap.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index edcad00c9830..e54d8320d0ff 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -1043,11 +1043,19 @@ impl api::IncomingWebhook for Bluesnap { | bluesnap::BluesnapWebhookEvents::Charge | bluesnap::BluesnapWebhookEvents::Chargeback | bluesnap::BluesnapWebhookEvents::ChargebackStatusChanged => { - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::PaymentAttemptId( - webhook_body.merchant_transaction_id, - ), - )) + if webhook_body.merchant_transaction_id.is_empty() { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + webhook_body.reference_number, + ), + )) + } else { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + webhook_body.merchant_transaction_id, + ), + )) + } } bluesnap::BluesnapWebhookEvents::Refund => { Ok(api_models::webhooks::ObjectReferenceId::RefundId( From 171d94f6457df91920597635e8160ff3bcf47369 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:14:11 +0530 Subject: [PATCH 309/443] ci: use git commands for pushing commits and tags in nightly release workflows (#3314) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../release-nightly-version-reusable.yml | 87 ++++--------------- .github/workflows/release-nightly-version.yml | 84 ++++-------------- 2 files changed, 35 insertions(+), 136 deletions(-) diff --git a/.github/workflows/release-nightly-version-reusable.yml b/.github/workflows/release-nightly-version-reusable.yml index deb8c44cc3c3..accd8c12a913 100644 --- a/.github/workflows/release-nightly-version-reusable.yml +++ b/.github/workflows/release-nightly-version-reusable.yml @@ -3,11 +3,8 @@ name: Create a nightly tag on: workflow_call: secrets: - app_id: - description: App ID for the GitHub app - required: true - app_private_key: - description: Private key for the GitHub app + token: + description: GitHub token for authenticating with GitHub required: true outputs: tag: @@ -31,23 +28,17 @@ jobs: runs-on: ubuntu-latest steps: - - name: Generate GitHub app token - id: generate_app_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.app_id }} - private-key: ${{ secrets.app_private_key }} - - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.token }} - name: Check if the workflow is run on an allowed branch shell: bash run: | - if [[ "${{github.ref}}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then - echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{github.ref}}'" + if [[ "${{ github.ref }}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then + echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{ github.ref }}'" exit 1 fi @@ -139,62 +130,22 @@ jobs: }' CHANGELOG.md rm release-notes.md - # We make use of GitHub API calls to commit and tag the changelog instead of the simpler - # `git commit`, `git tag` and `git push` commands to have signed commits and tags - - name: Commit generated changelog and create tag + - name: Set git configuration + shell: bash + run: | + git config --local user.name 'github-actions' + git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Commit, tag and push generated changelog shell: bash - env: - GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} run: | - HEAD_COMMIT="$(git rev-parse 'HEAD^{commit}')" - - # Create a tree based on the HEAD commit of the current branch and updated changelog file - TREE_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/trees' \ - --raw-field base_tree="${HEAD_COMMIT}" \ - --raw-field 'tree[][path]=CHANGELOG.md' \ - --raw-field 'tree[][mode]=100644' \ - --raw-field 'tree[][type]=blob' \ - --field 'tree[][content]=@CHANGELOG.md' \ - --jq '.sha' - )" - - # Create a commit to point to the above created tree - NEW_COMMIT_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/commits' \ - --raw-field "message=chore(version): ${NEXT_TAG}" \ - --raw-field "parents[]=${HEAD_COMMIT}" \ - --raw-field "tree=${TREE_SHA}" \ - --jq '.sha' - )" - - # Update the current branch to point to the above created commit - # We disable forced update so that the workflow will fail if the branch has been updated since the workflow started - # (for example, new commits were pushed to the branch after the workflow execution started). - gh api \ - --method PATCH \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/{owner}/{repo}/git/refs/heads/${ALLOWED_BRANCH_NAME}" \ - --raw-field "sha=${NEW_COMMIT_SHA}" \ - --field 'force=false' - - # Create a lightweight tag to point to the above created commit - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/refs' \ - --raw-field "ref=refs/tags/${NEXT_TAG}" \ - --raw-field "sha=${NEW_COMMIT_SHA}" + git add CHANGELOG.md + git commit --message "chore(version): ${NEXT_TAG}" + + git tag "${NEXT_TAG}" HEAD + + git push origin "${ALLOWED_BRANCH_NAME}" + git push origin "${NEXT_TAG}" - name: Set job outputs shell: bash diff --git a/.github/workflows/release-nightly-version.yml b/.github/workflows/release-nightly-version.yml index 36a843469d0c..13e844e7c5d7 100644 --- a/.github/workflows/release-nightly-version.yml +++ b/.github/workflows/release-nightly-version.yml @@ -27,23 +27,17 @@ jobs: runs-on: ubuntu-latest steps: - - name: Generate GitHub app token - id: generate_app_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} - private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} - - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.AUTO_RELEASE_PAT }} - name: Check if the workflow is run on an allowed branch shell: bash run: | - if [[ "${{github.ref}}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then - echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{github.ref}}'" + if [[ "${{ github.ref }}" != "refs/heads/${ALLOWED_BRANCH_NAME}" ]]; then + echo "::error::This workflow is expected to be run from the '${ALLOWED_BRANCH_NAME}' branch. Current branch: '${{ github.ref }}'" exit 1 fi @@ -80,66 +74,21 @@ jobs: echo "Postman collection files have no modifications" fi - - name: Commit updated Postman collections if modified + - name: Set git configuration shell: bash - env: - GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} if: ${{ env.POSTMAN_COLLECTION_FILES_UPDATED == 'true' }} run: | - # Obtain current HEAD commit SHA and use that as base tree SHA for creating a new tree - HEAD_COMMIT="$(git rev-parse 'HEAD^{commit}')" - UPDATED_TREE_SHA="${HEAD_COMMIT}" - - # Obtain the flags to be passed to the GitHub CLI. - # Each line contains the flags to be used corresponding to the file. - lines="$( - git ls-files \ - --format '--raw-field tree[][path]=%(path) --raw-field tree[][mode]=%(objectmode) --raw-field tree[][type]=%(objecttype) --field tree[][content]=@%(path)' \ - postman/collection-json - )" - - # Create a tree based on the HEAD commit of the current branch, using the contents of the updated Postman collections directory - while IFS= read -r line; do - # Split each line by space to obtain the flags passed to the GitHub CLI as an array - IFS=' ' read -ra flags <<< "${line}" - - # Create a tree by updating each collection JSON file. - # The SHA of the created tree is used as the base tree SHA for updating the next collection file. - UPDATED_TREE_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/trees' \ - --raw-field base_tree="${UPDATED_TREE_SHA}" \ - "${flags[@]}" \ - --jq '.sha' - )" - done <<< "${lines}" - - # Create a commit to point to the tree with all updated collections - NEW_COMMIT_SHA="$( - gh api \ - --method POST \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - '/repos/{owner}/{repo}/git/commits' \ - --raw-field "message=chore(postman): update Postman collection files" \ - --raw-field "parents[]=${HEAD_COMMIT}" \ - --raw-field "tree=${UPDATED_TREE_SHA}" \ - --jq '.sha' - )" - - # Update the current branch to point to the above created commit. - # We disable forced update so that the workflow will fail if the branch has been updated since the workflow started - # (for example, new commits were pushed to the branch after the workflow execution started). - gh api \ - --method PATCH \ - --header 'Accept: application/vnd.github+json' \ - --header 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/{owner}/{repo}/git/refs/heads/${ALLOWED_BRANCH_NAME}" \ - --raw-field "sha=${NEW_COMMIT_SHA}" \ - --field 'force=false' + git config --local user.name 'github-actions' + git config --local user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Commit and push updated Postman collections if modified + shell: bash + if: ${{ env.POSTMAN_COLLECTION_FILES_UPDATED == 'true' }} + run: | + git add postman + git commit --message 'chore(postman): update Postman collection files' + + git push origin "${ALLOWED_BRANCH_NAME}" create-nightly-tag: name: Create a nightly tag @@ -147,5 +96,4 @@ jobs: needs: - update-postman-collections secrets: - app_id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} - app_private_key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + token: ${{ secrets.AUTO_RELEASE_PAT }} From 8830a880d65521b78a2c5920417f24e19f3fe140 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 00:15:23 +0000 Subject: [PATCH 310/443] chore(version): 2024.01.11.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32504f7f0974..5e4f17884aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.11.0 + +### Features + +- **core:** Add new payments webhook events ([#3212](https://github.com/juspay/hyperswitch/pull/3212)) ([`e0e28b8`](https://github.com/juspay/hyperswitch/commit/e0e28b87c0647252918ef110cd7614c46b5cf943)) +- **payment_link:** Add status page for payment link ([#3213](https://github.com/juspay/hyperswitch/pull/3213)) ([`50e4d79`](https://github.com/juspay/hyperswitch/commit/50e4d797da31b570b5920b33d77c24a21d9871e2)) + +### Bug Fixes + +- **euclid_wasm:** Update braintree config prod ([#3288](https://github.com/juspay/hyperswitch/pull/3288)) ([`8830563`](https://github.com/juspay/hyperswitch/commit/8830563748ed20c40b7a21a66e9ad9fd02ddcf0e)) + +### Refactors + +- **connector:** [bluesnap] add connector_txn_id fallback for webhook ([#3315](https://github.com/juspay/hyperswitch/pull/3315)) ([`a69e876`](https://github.com/juspay/hyperswitch/commit/a69e876f8212cb94202686e073005c23b1b2fc35)) +- Removed basilisk feature ([#3281](https://github.com/juspay/hyperswitch/pull/3281)) ([`612f8d9`](https://github.com/juspay/hyperswitch/commit/612f8d9d5f5bcba78aa64c3128cc72be0f2860ea)) + +### Miscellaneous Tasks + +- Nits and small code improvements found during investigation of PR#3168 ([#3259](https://github.com/juspay/hyperswitch/pull/3259)) ([`fe3cf54`](https://github.com/juspay/hyperswitch/commit/fe3cf54781302c733c1682ded2c1735544407a5f)) + +**Full Changelog:** [`2024.01.10.0...2024.01.11.0`](https://github.com/juspay/hyperswitch/compare/2024.01.10.0...2024.01.11.0) + +- - - + ## 2024.01.10.0 ### Features From 61176524ca0c11c605538a1da9a267837193e1ec Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 11 Jan 2024 12:40:28 +0530 Subject: [PATCH 311/443] feat(payment_link): Added sdk layout option payment link (#3207) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Kashif --- crates/api_models/src/admin.rs | 5 ++ crates/api_models/src/payments.rs | 1 + crates/common_utils/src/consts.rs | 4 +- crates/diesel_models/src/payment_intent.rs | 2 +- crates/router/src/connector/utils.rs | 79 +++++++++---------- crates/router/src/core/payment_link.rs | 15 +++- .../src/core/payment_link/payment_link.html | 9 ++- crates/router/src/macros.rs | 5 +- .../up.sql | 2 +- .../up.sql | 2 +- .../down.sql | 2 +- .../up.sql | 2 +- openapi/openapi_spec.json | 14 +++- 13 files changed, 88 insertions(+), 54 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index c588bb87189f..134beacd226f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1175,6 +1175,9 @@ pub struct PaymentLinkConfigRequest { /// Custom merchant name for payment link #[schema(value_type = Option, max_length = 255, example = "hyperswitch")] pub seller_name: Option, + /// Custom layout for sdk + #[schema(value_type = Option, max_length = 255, example = "accordion")] + pub sdk_layout: Option, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, ToSchema)] @@ -1185,4 +1188,6 @@ pub struct PaymentLinkConfig { pub logo: String, /// Custom merchant name for payment link pub seller_name: String, + /// Custom layout for sdk + pub sdk_layout: String, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 4ef0c540b518..45611a91458f 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3381,6 +3381,7 @@ pub struct PaymentLinkDetails { pub max_items_visible_after_collapse: i8, pub theme: String, pub merchant_description: Option, + pub sdk_layout: String, } #[derive(Debug, serde::Serialize)] diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 169cb972c066..cd24e430b76d 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -48,8 +48,8 @@ pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/to /// Variable which store the card token for Prophetpay pub const PROPHETPAY_TOKEN: &str = "cctoken"; -/// Payment intent fulfillment default timeout (in seconds) -pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; +/// Default SDK Layout +pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; /// Payment intent default client secret expiry (in seconds) pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 6b546f90787e..17784bc56598 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -291,7 +291,7 @@ impl PaymentIntentUpdate { incremental_authorization_allowed: incremental_authorization_allowed .or(source.incremental_authorization_allowed), authorization_count: authorization_count.or(source.authorization_count), - session_expiry, + session_expiry: session_expiry.or(source.session_expiry), ..source } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 39b404d0f558..55173f9b339e 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1723,6 +1723,45 @@ impl FrmTransactionRouterDataRequest for fraud_check::FrmTransactionRouterData { } } +pub fn is_payment_failure(status: enums::AttemptStatus) -> bool { + match status { + common_enums::AttemptStatus::AuthenticationFailed + | common_enums::AttemptStatus::AuthorizationFailed + | common_enums::AttemptStatus::CaptureFailed + | common_enums::AttemptStatus::VoidFailed + | common_enums::AttemptStatus::Failure => true, + common_enums::AttemptStatus::Started + | common_enums::AttemptStatus::RouterDeclined + | common_enums::AttemptStatus::AuthenticationPending + | common_enums::AttemptStatus::AuthenticationSuccessful + | common_enums::AttemptStatus::Authorized + | common_enums::AttemptStatus::Charged + | common_enums::AttemptStatus::Authorizing + | common_enums::AttemptStatus::CodInitiated + | common_enums::AttemptStatus::Voided + | common_enums::AttemptStatus::VoidInitiated + | common_enums::AttemptStatus::CaptureInitiated + | common_enums::AttemptStatus::AutoRefunded + | common_enums::AttemptStatus::PartialCharged + | common_enums::AttemptStatus::PartialChargedAndChargeable + | common_enums::AttemptStatus::Unresolved + | common_enums::AttemptStatus::Pending + | common_enums::AttemptStatus::PaymentMethodAwaited + | common_enums::AttemptStatus::ConfirmationAwaited + | common_enums::AttemptStatus::DeviceDataCollectionPending => false, + } +} + +pub fn is_refund_failure(status: enums::RefundStatus) -> bool { + match status { + common_enums::RefundStatus::Failure | common_enums::RefundStatus::TransactionFailure => { + true + } + common_enums::RefundStatus::ManualReview + | common_enums::RefundStatus::Pending + | common_enums::RefundStatus::Success => false, + } +} #[cfg(test)] mod error_code_error_message_tests { #![allow(clippy::unwrap_used)] @@ -1802,43 +1841,3 @@ mod error_code_error_message_tests { assert_eq!(error_code_error_message_none, None); } } - -pub fn is_payment_failure(status: enums::AttemptStatus) -> bool { - match status { - common_enums::AttemptStatus::AuthenticationFailed - | common_enums::AttemptStatus::AuthorizationFailed - | common_enums::AttemptStatus::CaptureFailed - | common_enums::AttemptStatus::VoidFailed - | common_enums::AttemptStatus::Failure => true, - common_enums::AttemptStatus::Started - | common_enums::AttemptStatus::RouterDeclined - | common_enums::AttemptStatus::AuthenticationPending - | common_enums::AttemptStatus::AuthenticationSuccessful - | common_enums::AttemptStatus::Authorized - | common_enums::AttemptStatus::Charged - | common_enums::AttemptStatus::Authorizing - | common_enums::AttemptStatus::CodInitiated - | common_enums::AttemptStatus::Voided - | common_enums::AttemptStatus::VoidInitiated - | common_enums::AttemptStatus::CaptureInitiated - | common_enums::AttemptStatus::AutoRefunded - | common_enums::AttemptStatus::PartialCharged - | common_enums::AttemptStatus::PartialChargedAndChargeable - | common_enums::AttemptStatus::Unresolved - | common_enums::AttemptStatus::Pending - | common_enums::AttemptStatus::PaymentMethodAwaited - | common_enums::AttemptStatus::ConfirmationAwaited - | common_enums::AttemptStatus::DeviceDataCollectionPending => false, - } -} - -pub fn is_refund_failure(status: enums::RefundStatus) -> bool { - match status { - common_enums::RefundStatus::Failure | common_enums::RefundStatus::TransactionFailure => { - true - } - common_enums::RefundStatus::ManualReview - | common_enums::RefundStatus::Pending - | common_enums::RefundStatus::Success => false, - } -} diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 9adf9031793b..84cd726a7e49 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,7 +1,7 @@ use api_models::admin as admin_types; use common_utils::{ consts::{ - DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, DEFAULT_SESSION_EXPIRY, }, ext_traits::{OptionExt, ValueExt}, @@ -85,6 +85,7 @@ pub async fn intiate_payment_link_flow( theme: DEFAULT_BACKGROUND_COLOR.to_string(), logo: DEFAULT_MERCHANT_LOGO.to_string(), seller_name: merchant_name_from_merchant_account, + sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(), } }; @@ -180,6 +181,7 @@ pub async fn intiate_payment_link_flow( max_items_visible_after_collapse: 3, theme: payment_link_config.clone().theme, merchant_description: payment_intent.description, + sdk_layout: payment_link_config.clone().sdk_layout, }; let js_script = get_js_script(api_models::payments::PaymentLinkData::PaymentLinkDetails( @@ -384,10 +386,21 @@ pub fn get_payment_link_config_based_on_priority( }) .unwrap_or(merchant_name.clone()); + let sdk_layout = payment_create_link_config + .as_ref() + .and_then(|pc_config| pc_config.config.sdk_layout.clone()) + .or_else(|| { + business_config + .as_ref() + .and_then(|business_config| business_config.sdk_layout.clone()) + }) + .unwrap_or(DEFAULT_SDK_LAYOUT.to_owned()); + let payment_link_config = admin_types::PaymentLinkConfig { theme, logo, seller_name, + sdk_layout, }; Ok((payment_link_config, domain_name)) diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 3a3ed4fffe05..f6e62f8bdc8a 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -1269,8 +1269,15 @@ appearance: appearance, clientSecret: client_secret, }); + var type = (paymentDetails.sdk_layout === "spaced_accordion" || paymentDetails.sdk_layout === "accordion") + ? "accordion" + : paymentDetails.sdk_layout; + var unifiedCheckoutOptions = { - layout: "tabs", + layout: { + type: type, //accordion , tabs, spaced accordion + spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion" + }, branding: "never", wallets: { walletReturnUrl: paymentDetails.return_url, diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index e6c9dba7d6e2..efe71e49bb04 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,4 +1 @@ -pub use common_utils::{ - async_spawn, collect_missing_value_keys, fallback_reverse_lookup_not_found, newtype, - newtype_impl, -}; +pub use common_utils::{collect_missing_value_keys, newtype}; diff --git a/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql b/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql index 19fbedccbbfe..40e65c149f26 100644 --- a/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql +++ b/migrations/2023-11-24-112541_add_payment_config_business_profile/up.sql @@ -1,3 +1,3 @@ -- Your SQL goes here ALTER TABLE business_profile -ADD COLUMN IF NOT EXISTS payment_link_config JSONB DEFAULT NULL; \ No newline at end of file +ADD COLUMN IF NOT EXISTS payment_link_config JSONB DEFAULT NULL; diff --git a/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql b/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql index b48346c763ed..207fdc8817e1 100644 --- a/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql +++ b/migrations/2023-11-24-115538_add_profile_id_payment_link/up.sql @@ -1,2 +1,2 @@ -- Your SQL goes here -ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64) DEFAULT NULL; \ No newline at end of file +ALTER TABLE payment_link ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64) DEFAULT NULL; diff --git a/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql index 2801a68c67ee..6af3e1e7f3df 100644 --- a/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql +++ b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -ALTER TABLE payment_intent DROP COLUMN IF EXISTS session_expiry; \ No newline at end of file +ALTER TABLE payment_intent DROP COLUMN IF EXISTS session_expiry; diff --git a/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql index f2ee81e847d8..e6ad0a728d44 100644 --- a/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql +++ b/migrations/2023-12-06-112810_add_intent_fullfilment_time_payment_intent/up.sql @@ -1,2 +1,2 @@ -- Your SQL goes here -ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS session_expiry TIMESTAMP DEFAULT NULL; \ No newline at end of file +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS session_expiry TIMESTAMP DEFAULT NULL; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index df5b9448971d..dd27b5d609d8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8686,7 +8686,8 @@ "required": [ "theme", "logo", - "seller_name" + "seller_name", + "sdk_layout" ], "properties": { "theme": { @@ -8700,6 +8701,10 @@ "seller_name": { "type": "string", "description": "Custom merchant name for payment link" + }, + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk" } } }, @@ -8726,6 +8731,13 @@ "example": "hyperswitch", "nullable": true, "maxLength": 255 + }, + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk", + "example": "accordion", + "nullable": true, + "maxLength": 255 } } }, From 5a1a3da7502ce9e13546b896477d82719162d5b6 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:57:36 +0530 Subject: [PATCH 312/443] fix(core): surcharge with saved card failure (#3318) --- crates/router/src/core/payments.rs | 3 +- crates/router/src/core/payments/helpers.rs | 51 +++++++--------------- 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 67328e356128..a07c88ea6679 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -509,8 +509,7 @@ where let raw_card_key = payment_data .payment_method_data .as_ref() - .map(get_key_params_for_surcharge_details) - .transpose()? + .and_then(get_key_params_for_surcharge_details) .map(|(payment_method, payment_method_type, card_network)| { types::SurchargeKey::PaymentMethodData( payment_method, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d864cacc52fd..fed8357bc388 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3610,93 +3610,74 @@ impl ApplePayData { pub fn get_key_params_for_surcharge_details( payment_method_data: &api_models::payments::PaymentMethodData, -) -> RouterResult<( +) -> Option<( common_enums::PaymentMethod, common_enums::PaymentMethodType, Option, )> { match payment_method_data { api_models::payments::PaymentMethodData::Card(card) => { - let card_network = card - .card_network - .clone() - .get_required_value("payment_method_data.card.card_network")?; // surcharge generated will always be same for credit as well as debit // since surcharge conditions cannot be defined on card_type - Ok(( + Some(( common_enums::PaymentMethod::Card, common_enums::PaymentMethodType::Credit, - Some(card_network), + card.card_network.clone(), )) } - api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( + api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Some(( common_enums::PaymentMethod::CardRedirect, card_redirect_data.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::Wallet(wallet) => Ok(( + api_models::payments::PaymentMethodData::Wallet(wallet) => Some(( common_enums::PaymentMethod::Wallet, wallet.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok(( + api_models::payments::PaymentMethodData::PayLater(pay_later) => Some(( common_enums::PaymentMethod::PayLater, pay_later.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Ok(( + api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Some(( common_enums::PaymentMethod::BankRedirect, bank_redirect.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Ok(( + api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Some(( common_enums::PaymentMethod::BankDebit, bank_debit.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Ok(( + api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Some(( common_enums::PaymentMethod::BankTransfer, bank_transfer.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::Crypto(crypto) => Ok(( + api_models::payments::PaymentMethodData::Crypto(crypto) => Some(( common_enums::PaymentMethod::Crypto, crypto.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::MandatePayment => { - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - } - .into()) - } - api_models::payments::PaymentMethodData::Reward => { - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - } - .into()) - } - api_models::payments::PaymentMethodData::Upi(_) => Ok(( + api_models::payments::PaymentMethodData::MandatePayment => None, + api_models::payments::PaymentMethodData::Reward => None, + api_models::payments::PaymentMethodData::Upi(_) => Some(( common_enums::PaymentMethod::Upi, common_enums::PaymentMethodType::UpiCollect, None, )), - api_models::payments::PaymentMethodData::Voucher(voucher) => Ok(( + api_models::payments::PaymentMethodData::Voucher(voucher) => Some(( common_enums::PaymentMethod::Voucher, voucher.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok(( + api_models::payments::PaymentMethodData::GiftCard(gift_card) => Some(( common_enums::PaymentMethod::GiftCard, gift_card.get_payment_method_type(), None, )), - api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data", - } - .into()) - } + api_models::payments::PaymentMethodData::CardToken(_) => None, } } From 8626bda6d5aa9e7531edc7ea50ed4f30c3b7227a Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 11 Jan 2024 13:00:41 +0530 Subject: [PATCH 313/443] refactor(router): flagged order_details validation to skip validation (#3116) --- crates/router/src/core/payments/helpers.rs | 23 +++++++++++-------- .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + .../payments/operations/payment_update.rs | 1 + 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index fed8357bc388..ec6371f310f2 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3742,17 +3742,22 @@ pub async fn get_gsm_record( pub fn validate_order_details_amount( order_details: Vec, amount: i64, + should_validate: bool, ) -> Result<(), errors::ApiErrorResponse> { - let total_order_details_amount: i64 = order_details - .iter() - .map(|order| order.amount * i64::from(order.quantity)) - .sum(); + if should_validate { + let total_order_details_amount: i64 = order_details + .iter() + .map(|order| order.amount * i64::from(order.quantity)) + .sum(); - if total_order_details_amount != amount { - Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Total sum of order details doesn't match amount in payment request" - .to_string(), - }) + if total_order_details_amount != amount { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Total sum of order details doesn't match amount in payment request" + .to_string(), + }) + } else { + Ok(()) + } } else { Ok(()) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 0970a952c8e0..00ae8da6ae49 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -104,6 +104,7 @@ impl helpers::validate_order_details_amount( order_details.to_owned(), payment_intent.amount, + false, )?; } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 94436026dc4a..09ec436ed001 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -245,6 +245,7 @@ impl helpers::validate_order_details_amount( order_details.to_owned(), payment_intent.amount, + false, )?; } diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 5ed0c45d4e26..afb83d38dc5e 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -64,6 +64,7 @@ impl helpers::validate_order_details_amount( order_details.to_owned(), payment_intent.amount, + false, )?; } From 4f9c04b856761b9c0486abad4c36de191da2c460 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:39:46 +0530 Subject: [PATCH 314/443] fix(router): add config to avoid connector tokenization for `apple pay` `simplified flow` (#3234) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 2 +- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/router/src/configs/settings.rs | 9 +++++ crates/router/src/core/payments.rs | 40 ++++++++++++++----- .../router/src/core/payments/transformers.rs | 2 +- loadtest/config/development.toml | 2 +- 7 files changed, 45 insertions(+), 14 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 7e32b2f5d3b1..94f71fa3f704 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -339,7 +339,7 @@ sts_role_session_name = "" # An identifier for the assumed role session, used to #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } -checkout = { long_lived_token = false, payment_method = "wallet" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } mollie = { long_lived_token = false, payment_method = "card" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } square = { long_lived_token = false, payment_method = "card" } diff --git a/config/development.toml b/config/development.toml index ebd4cb1c93e6..272b36417137 100644 --- a/config/development.toml +++ b/config/development.toml @@ -415,7 +415,7 @@ debit = { currency = "USD" } [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } -checkout = { long_lived_token = false, payment_method = "wallet" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } mollie = {long_lived_token = false, payment_method = "card"} square = {long_lived_token = false, payment_method = "card"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a8cf5bfb0519..e55353f89033 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -229,7 +229,7 @@ consumer_group = "SCHEDULER_GROUP" #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } -checkout = { long_lived_token = false, payment_method = "wallet" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } mollie = {long_lived_token = false, payment_method = "card"} stax = { long_lived_token = true, payment_method = "card,bank_debit" } square = {long_lived_token = false, payment_method = "card"} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index b7aa3d3ea5dd..3d93c2f188b7 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -287,6 +287,15 @@ pub struct PaymentMethodTokenFilter { pub payment_method: HashSet, pub payment_method_type: Option, pub long_lived_token: bool, + pub apple_pay_pre_decrypt_flow: Option, +} + +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum ApplePayPreDecryptFlow { + #[default] + ConnectorTokenization, + NetworkTokenization, } #[derive(Debug, Deserialize, Clone, Default)] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a07c88ea6679..ff4934e1efcb 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -44,7 +44,7 @@ use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs #[cfg(feature = "frm")] use crate::core::fraud_check as frm_core; use crate::{ - configs::settings::PaymentMethodTypeTokenFilter, + configs::settings::{ApplePayPreDecryptFlow, PaymentMethodTypeTokenFilter}, core::{ errors::{self, CustomResult, RouterResponse, RouterResult}, payment_methods::PaymentMethodRetrieve, @@ -1582,6 +1582,7 @@ fn is_payment_method_tokenization_enabled_for_connector( connector_name: &str, payment_method: &storage::enums::PaymentMethod, payment_method_type: &Option, + apple_pay_flow: &Option, ) -> RouterResult { let connector_tokenization_filter = state.conf.tokenization.0.get(connector_name); @@ -1595,13 +1596,35 @@ fn is_payment_method_tokenization_enabled_for_connector( payment_method_type, connector_filter.payment_method_type.clone(), ) + && is_apple_pay_pre_decrypt_type_connector_tokenization( + payment_method_type, + apple_pay_flow, + connector_filter.apple_pay_pre_decrypt_flow.clone(), + ) }) .unwrap_or(false)) } +fn is_apple_pay_pre_decrypt_type_connector_tokenization( + payment_method_type: &Option, + apple_pay_flow: &Option, + apple_pay_pre_decrypt_flow_filter: Option, +) -> bool { + match (payment_method_type, apple_pay_flow) { + ( + Some(storage::enums::PaymentMethodType::ApplePay), + Some(enums::ApplePayFlow::Simplified), + ) => !matches!( + apple_pay_pre_decrypt_flow_filter, + Some(ApplePayPreDecryptFlow::NetworkTokenization) + ), + _ => true, + } +} + fn decide_apple_pay_flow( payment_method_type: &Option, - merchant_connector_account: &Option, + merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, ) -> Option { payment_method_type.and_then(|pmt| match pmt { api_models::enums::PaymentMethodType::ApplePay => { @@ -1612,9 +1635,9 @@ fn decide_apple_pay_flow( } fn check_apple_pay_metadata( - merchant_connector_account: &Option, + merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>, ) -> Option { - merchant_connector_account.clone().and_then(|mca| { + merchant_connector_account.and_then(|mca| { let metadata = mca.get_metadata(); metadata.and_then(|apple_pay_metadata| { let parsed_metadata = apple_pay_metadata @@ -1785,19 +1808,18 @@ where .get_required_value("payment_method")?; let payment_method_type = &payment_data.payment_attempt.payment_method_type; + let apple_pay_flow = + decide_apple_pay_flow(payment_method_type, Some(merchant_connector_account)); + let is_connector_tokenization_enabled = is_payment_method_tokenization_enabled_for_connector( state, &connector, payment_method, payment_method_type, + &apple_pay_flow, )?; - let apple_pay_flow = decide_apple_pay_flow( - payment_method_type, - &Some(merchant_connector_account.clone()), - ); - add_apple_pay_flow_metrics( &apple_pay_flow, payment_data.payment_attempt.connector.clone(), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 7b7d64a5f81a..551f8cd5da45 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -118,7 +118,7 @@ where let apple_pay_flow = payments::decide_apple_pay_flow( &payment_data.payment_attempt.payment_method_type, - &Some(merchant_connector_account.clone()), + Some(merchant_connector_account), ); router_data = types::RouterData { diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 066933317b02..358a591a6678 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -200,7 +200,7 @@ red_pagos = { country = "UY", currency = "UYU" } #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } -checkout = { long_lived_token = false, payment_method = "wallet" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } mollie = {long_lived_token = false, payment_method = "card"} braintree = { long_lived_token = false, payment_method = "card" } gocardless = {long_lived_token = true, payment_method = "bank_debit"} From 5a5400cf5b539996b2f327c51d4a07b4a86fd1be Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:57:56 +0530 Subject: [PATCH 315/443] feat(connector): [BOA/Cyb] Include merchant metadata in capture and void requests (#3308) --- .../connector/bankofamerica/transformers.rs | 15 ++++++++++++ .../src/connector/cybersource/transformers.rs | 23 +++++++++++++++++++ .../router/src/core/payments/transformers.rs | 2 ++ crates/router/src/types.rs | 4 ++++ 4 files changed, 44 insertions(+) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 71a44b5a6e67..e024eb7a5019 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -988,6 +988,8 @@ pub struct OrderInformation { pub struct BankOfAmericaCaptureRequest { order_information: OrderInformation, client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> @@ -997,6 +999,10 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> fn try_from( value: &BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>, ) -> Result { + let merchant_defined_information = + value.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { order_information: OrderInformation { amount_details: Amount { @@ -1007,6 +1013,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> client_reference_information: ClientReferenceInformation { code: Some(value.router_data.connector_request_reference_id.clone()), }, + merchant_defined_information, }) } } @@ -1016,6 +1023,9 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCaptureRouterData>> pub struct BankOfAmericaVoidRequest { client_reference_information: ClientReferenceInformation, reversal_information: ReversalInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, + // The connector documentation does not mention the merchantDefinedInformation field for Void requests. But this has been still added because it works! } #[derive(Debug, Serialize)] @@ -1032,6 +1042,10 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> fn try_from( value: &BankOfAmericaRouterData<&types::PaymentsCancelRouterData>, ) -> Result { + let merchant_defined_information = + value.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { client_reference_information: ClientReferenceInformation { code: Some(value.router_data.connector_request_reference_id.clone()), @@ -1054,6 +1068,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCancelRouterData>> field_name: "Cancellation Reason", })?, }, + merchant_defined_information, }) } } diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index e46833d2ecde..bc69fb78129f 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -837,6 +837,9 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> pub struct CybersourcePaymentsCaptureRequest { processing_information: ProcessingInformation, order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, } #[derive(Debug, Serialize)] @@ -853,6 +856,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> fn try_from( item: &CybersourceRouterData<&types::PaymentsCaptureRouterData>, ) -> Result { + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { processing_information: ProcessingInformation { capture_options: Some(CaptureOptions { @@ -873,6 +880,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> }, bill_to: None, }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }, + merchant_defined_information, }) } } @@ -918,6 +929,9 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout pub struct CybersourceVoidRequest { client_reference_information: ClientReferenceInformation, reversal_information: ReversalInformation, + #[serde(skip_serializing_if = "Option::is_none")] + merchant_defined_information: Option>, + // The connector documentation does not mention the merchantDefinedInformation field for Void requests. But this has been still added because it works! } #[derive(Debug, Serialize)] @@ -932,6 +946,10 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCancelRouterData>> for Cyber fn try_from( value: &CybersourceRouterData<&types::PaymentsCancelRouterData>, ) -> Result { + let merchant_defined_information = + value.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); Ok(Self { client_reference_information: ClientReferenceInformation { code: Some(value.router_data.connector_request_reference_id.clone()), @@ -954,6 +972,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCancelRouterData>> for Cyber field_name: "Cancellation Reason", })?, }, + merchant_defined_information, }) } } @@ -1591,6 +1610,7 @@ impl #[serde(rename_all = "camelCase")] pub struct CybersourceRefundRequest { order_information: OrderInformation, + client_reference_information: ClientReferenceInformation, } impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for CybersourceRefundRequest { @@ -1605,6 +1625,9 @@ impl TryFrom<&CybersourceRouterData<&types::RefundsRouterData>> for Cybers currency: item.router_data.request.currency, }, }, + client_reference_information: ClientReferenceInformation { + code: Some(item.router_data.request.refund_id.clone()), + }, }) } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 551f8cd5da45..359373e469b7 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1223,6 +1223,7 @@ impl TryFrom> for types::PaymentsCaptureD None => None, }, browser_info, + metadata: payment_data.payment_intent.metadata, }) } } @@ -1257,6 +1258,7 @@ impl TryFrom> for types::PaymentsCancelDa cancellation_reason: payment_data.payment_attempt.cancellation_reason, connector_meta: payment_data.payment_attempt.connector_metadata, browser_info, + metadata: payment_data.payment_intent.metadata, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 2225c2965bcf..7cd45a0192f0 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -428,6 +428,8 @@ pub struct PaymentsCaptureData { pub multiple_capture_data: Option, pub connector_meta: Option, pub browser_info: Option, + pub metadata: Option, + // This metadata is used to store the metadata shared during the payment intent request. } #[derive(Debug, Clone, Default)] @@ -542,6 +544,8 @@ pub struct PaymentsCancelData { pub cancellation_reason: Option, pub connector_meta: Option, pub browser_info: Option, + pub metadata: Option, + // This metadata is used to store the metadata shared during the payment intent request. } #[derive(Debug, Default, Clone)] From 9eaebe8db3d83105ef1e8fc784241e1fb795dd22 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 11 Jan 2024 13:58:56 +0530 Subject: [PATCH 316/443] refactor(router): restricted list payment method Customer to api-key based (#3100) --- crates/api_models/src/payment_methods.rs | 6 +----- crates/router/src/routes/payment_methods.rs | 8 +------- openapi/openapi_spec.json | 18 ------------------ 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 85b0adefca5f..a907fff60193 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -13,9 +13,7 @@ use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; use crate::{ - admin, - customers::CustomerId, - enums as api_enums, + admin, enums as api_enums, payments::{self, BankCodeResponse}, }; @@ -459,8 +457,6 @@ pub struct RequestPaymentMethodTypes { #[derive(Debug, Clone, serde::Serialize, Default, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodListRequest { - #[serde(skip_deserializing)] - pub customer_id: Option, /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893ein2d")] pub client_secret: Option, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 43a7272a4435..a6eeeabd687f 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -108,7 +108,6 @@ pub async fn list_payment_method_api( get, path = "/customers/{customer_id}/payment_methods", params ( - ("customer_id" = String, Path, description = "The unique identifier for the customer account"), ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), @@ -134,10 +133,6 @@ pub async fn list_customer_payment_method_api( ) -> HttpResponse { let flow = Flow::CustomerPaymentMethodsList; let payload = query_payload.into_inner(); - let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { - Ok((auth, _auth_flow)) => (auth, _auth_flow), - Err(e) => return api::log_and_return_error_response(e), - }; let customer_id = customer_id.into_inner().0; Box::pin(api::server_wrap( flow, @@ -153,7 +148,7 @@ pub async fn list_customer_payment_method_api( Some(&customer_id), ) }, - &*auth, + &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await @@ -166,7 +161,6 @@ pub async fn list_customer_payment_method_api( path = "/customers/payment_methods", params ( ("client-secret" = String, Path, description = "A secret known only to your application and the authorization server"), - ("customer_id" = String, Path, description = "The unique identifier for the customer account"), ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index dd27b5d609d8..4423d1177c91 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -473,15 +473,6 @@ "type": "string" } }, - { - "name": "customer_id", - "in": "path", - "description": "The unique identifier for the customer account", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "accepted_country", "in": "query", @@ -711,15 +702,6 @@ "description": "List payment methods for a Customer\n\nTo filter and list the applicable payment methods for a particular Customer ID", "operationId": "List all Payment Methods for a Customer", "parameters": [ - { - "name": "customer_id", - "in": "path", - "description": "The unique identifier for the customer account", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "accepted_country", "in": "query", From ed07c5ba90868a3132ca90d72219db3ba8978232 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:03:46 +0530 Subject: [PATCH 317/443] feat(euclid_wasm): config changes for NMI (#3329) --- crates/connector_configs/toml/development.toml | 3 ++- crates/connector_configs/toml/production.toml | 7 +++---- crates/connector_configs/toml/sandbox.toml | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index b24de92de101..2d1363f5831e 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1246,8 +1246,9 @@ label="apple" payment_method_type = "apple_pay" [[nmi.wallet]] payment_method_type = "google_pay" -[nmi.connector_auth.HeaderKey] +[nmi.connector_auth.BodyKey] api_key="API Key" +key1="Public Key" [nmi.connector_webhook_details] merchant_secret="Source verification key" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index cbc2bb238021..d4261cb0d94d 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1062,10 +1062,9 @@ label="apple" [nmi] [[nmi.bank_redirect]] payment_method_type = "ideal" -[nmi.connector_auth.SignatureKey] -api_key="Client ID" -key1="Airline ID" -api_secret="Client Secret" +[nmi.connector_auth.BodyKey] +api_key="API Key" +key1="Public Key" [nmi.connector_webhook_details] merchant_secret="Source verification key" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index c41ad7793e8e..41bc954cc90d 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1242,8 +1242,9 @@ label="apple" payment_method_type = "apple_pay" [[nmi.wallet]] payment_method_type = "google_pay" -[nmi.connector_auth.HeaderKey] +[nmi.connector_auth.BodyKey] api_key="API Key" +key1="Public Key" [nmi.connector_webhook_details] merchant_secret="Source verification key" From 6a4706323c61f3722dc543993c55084dc9ff9850 Mon Sep 17 00:00:00 2001 From: Rachit Naithani <81706961+racnan@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:26:31 +0530 Subject: [PATCH 318/443] feat(users): invite user without email (#3328) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/user.rs | 1 + crates/router/src/core/user.rs | 52 +++++++++++++++----------- crates/router/src/routes/app.rs | 2 +- crates/router/src/routes/user.rs | 1 - crates/router/src/types/domain/user.rs | 7 +++- 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 07909a35782e..f5af31c8e7f6 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -86,6 +86,7 @@ pub struct InviteUserRequest { #[derive(Debug, serde::Serialize)] pub struct InviteUserResponse { pub is_email_sent: bool, + pub password: Option>, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 532f8208ecf1..b1a582cedecf 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,7 +1,5 @@ use api_models::user as user_api; -#[cfg(feature = "email")] -use diesel_models::user_role::UserRoleNew; -use diesel_models::{enums::UserStatus, user as storage_user}; +use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; #[cfg(feature = "email")] use error_stack::IntoReport; use error_stack::ResultExt; @@ -342,7 +340,6 @@ pub async fn reset_password( Ok(ApplicationResponse::StatusOk) } -#[cfg(feature = "email")] pub async fn invite_user( state: AppState, request: user_api::InviteUserRequest, @@ -395,6 +392,7 @@ pub async fn invite_user( Ok(ApplicationResponse::Json(user_api::InviteUserResponse { is_email_sent: false, + password: None, })) } else if invitee_user .as_ref() @@ -432,25 +430,37 @@ pub async fn invite_user( } })?; - let email_contents = email_types::InviteUser { - recipient_email: invitee_email, - user_name: domain::UserName::new(new_user.get_name())?, - settings: state.conf.clone(), - subject: "You have been invited to join Hyperswitch Community!", - }; - - let send_email_result = state - .email_client - .compose_and_send_email( - Box::new(email_contents), - state.conf.proxy.https_url.as_ref(), - ) - .await; - - logger::info!(?send_email_result); + let is_email_sent; + #[cfg(feature = "email")] + { + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + }; + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + logger::info!(?send_email_result); + is_email_sent = send_email_result.is_ok(); + } + #[cfg(not(feature = "email"))] + { + is_email_sent = false; + } Ok(ApplicationResponse::Json(user_api::InviteUserResponse { - is_email_sent: send_email_result.is_ok(), + is_email_sent, + password: if cfg!(not(feature = "email")) { + Some(new_user.get_password().get_secret()) + } else { + None + }, })) } else { Err(UserErrors::InternalServerError.into()) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 6625a206be21..015e3305de10 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -879,6 +879,7 @@ impl User { .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) + .service(web::resource("/user/invite").route(web::post().to(invite_user))) .service( web::resource("/data") .route(web::get().to(get_multiple_dashboard_metadata)) @@ -901,7 +902,6 @@ impl User { ) .service(web::resource("/forgot_password").route(web::post().to(forgot_password))) .service(web::resource("/reset_password").route(web::post().to(reset_password))) - .service(web::resource("/user/invite").route(web::post().to(invite_user))) .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 7f0f0db3b69e..a77b82c550e6 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -333,7 +333,6 @@ pub async fn reset_password( .await } -#[cfg(feature = "email")] pub async fn invite_user( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 8f204814ec40..d271ed5e29d1 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -489,6 +489,10 @@ impl NewUser { self.new_merchant.clone() } + pub fn get_password(&self) -> UserPassword { + self.password.clone() + } + pub async fn insert_user_in_db( &self, db: &dyn StorageInterface, @@ -683,8 +687,7 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.0.email.clone().try_into()?; let name = UserName::new(value.0.name.clone())?; - let password = password::generate_password_hash(uuid::Uuid::new_v4().to_string().into())?; - let password = UserPassword::new(password)?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { From e376f68c167a289957a4372df108797088ab1f6e Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:31:35 +0530 Subject: [PATCH 319/443] feat(connector): [Volt] Add support for refund webhooks (#3326) --- crates/router/src/connector/volt.rs | 55 +++++--- .../router/src/connector/volt/transformers.rs | 120 +++++++++++------- 2 files changed, 115 insertions(+), 60 deletions(-) diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 3641c0c3ddc3..39296bb64340 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -635,21 +635,44 @@ impl api::IncomingWebhook for Volt { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let webhook_body: volt::VoltWebhookBodyReference = request - .body - .parse_struct("VoltWebhookBodyReference") - .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; - let reference = match webhook_body.merchant_internal_reference { - Some(merchant_internal_reference) => { - api_models::payments::PaymentIdType::PaymentAttemptId(merchant_internal_reference) - } - None => { - api_models::payments::PaymentIdType::ConnectorTransactionId(webhook_body.payment) - } - }; - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - reference, - )) + let x_volt_type = + utils::get_header_key_value(webhook_headers::X_VOLT_TYPE, request.headers)?; + if x_volt_type == "refund_confirmed" || x_volt_type == "refund_failed" { + let refund_webhook_body: volt::VoltRefundWebhookBodyReference = request + .body + .parse_struct("VoltRefundWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + let refund_reference = match refund_webhook_body.external_reference { + Some(external_reference) => { + api_models::webhooks::RefundIdType::RefundId(external_reference) + } + None => api_models::webhooks::RefundIdType::ConnectorRefundId( + refund_webhook_body.refund, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + refund_reference, + )) + } else { + let webhook_body: volt::VoltPaymentWebhookBodyReference = request + .body + .parse_struct("VoltPaymentWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + let reference = match webhook_body.merchant_internal_reference { + Some(merchant_internal_reference) => { + api_models::payments::PaymentIdType::PaymentAttemptId( + merchant_internal_reference, + ) + } + None => api_models::payments::PaymentIdType::ConnectorTransactionId( + webhook_body.payment, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + reference, + )) + } } fn get_webhook_event_type( @@ -663,7 +686,7 @@ impl api::IncomingWebhook for Volt { .body .parse_struct("VoltWebhookBodyEventType") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - Ok(api::IncomingWebhookEvent::from(payload.status)) + Ok(api::IncomingWebhookEvent::from(payload)) } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index 4c6eaeb52f48..8b9bbecb0889 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -46,6 +46,7 @@ pub mod webhook_headers { pub const X_VOLT_SIGNED: &str = "X-Volt-Signed"; pub const X_VOLT_TIMED: &str = "X-Volt-Timed"; pub const USER_AGENT: &str = "User-Agent"; + pub const X_VOLT_TYPE: &str = "X-Volt-Type"; } #[derive(Debug, Serialize)] @@ -318,8 +319,8 @@ pub enum VoltPaymentStatus { #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum VoltPaymentsResponseData { - WebhookResponse(VoltWebhookObjectResource), PsyncResponse(VoltPsyncResponse), + WebhookResponse(VoltPaymentWebhookObjectResource), } #[derive(Debug, Serialize, Clone, Deserialize)] @@ -418,13 +419,16 @@ impl } } } - -impl From for enums::AttemptStatus { - fn from(status: VoltWebhookStatus) -> Self { +impl From for enums::AttemptStatus { + fn from(status: VoltWebhookPaymentStatus) -> Self { match status { - VoltWebhookStatus::Completed | VoltWebhookStatus::Received => Self::Charged, - VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => Self::Failure, - VoltWebhookStatus::Pending => Self::Pending, + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { + Self::Charged + } + VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { + Self::Failure + } + VoltWebhookPaymentStatus::Pending => Self::Pending, } } } @@ -432,6 +436,7 @@ impl From for enums::AttemptStatus { // REFUND : // Type definition for RefundRequest #[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct VoltRefundRequest { pub amount: i64, pub external_reference: String, @@ -447,28 +452,6 @@ impl TryFrom<&VoltRouterData<&types::RefundsRouterData>> for VoltRefundReq } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping - } - } -} - #[derive(Default, Debug, Clone, Deserialize)] pub struct RefundResponse { id: String, @@ -492,30 +475,66 @@ impl TryFrom> } #[derive(Debug, Deserialize, Clone, Serialize)] -pub struct VoltWebhookBodyReference { +#[serde(rename_all = "camelCase")] +pub struct VoltPaymentWebhookBodyReference { pub payment: String, pub merchant_internal_reference: Option, } +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltRefundWebhookBodyReference { + pub refund: String, + pub external_reference: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum VoltWebhookBodyEventType { + Payment(VoltPaymentsWebhookBodyEventType), + Refund(VoltRefundsWebhookBodyEventType), +} + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct VoltWebhookBodyEventType { - pub status: VoltWebhookStatus, +pub struct VoltPaymentsWebhookBodyEventType { + pub status: VoltWebhookPaymentStatus, pub detailed_status: Option, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltRefundsWebhookBodyEventType { + pub status: VoltWebhookRefundsStatus, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum VoltWebhookObjectResource { + Payment(VoltPaymentWebhookObjectResource), + Refund(VoltRefundWebhookObjectResource), +} + #[derive(Debug, Deserialize, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct VoltWebhookObjectResource { +pub struct VoltPaymentWebhookObjectResource { pub payment: String, pub merchant_internal_reference: Option, - pub status: VoltWebhookStatus, + pub status: VoltWebhookPaymentStatus, pub detailed_status: Option, } +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VoltRefundWebhookObjectResource { + pub refund: String, + pub external_reference: Option, + pub status: VoltWebhookRefundsStatus, +} + #[derive(Debug, Deserialize, Clone, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum VoltWebhookStatus { +pub enum VoltWebhookPaymentStatus { Completed, Failed, Pending, @@ -523,6 +542,13 @@ pub enum VoltWebhookStatus { NotReceived, } +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VoltWebhookRefundsStatus { + RefundConfirmed, + RefundFailed, +} + #[derive(Debug, Deserialize, Clone, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[derive(strum::Display)] @@ -539,16 +565,22 @@ pub enum VoltDetailedStatus { AwaitingCheckoutAuthorisation, } -impl From for api::IncomingWebhookEvent { - fn from(status: VoltWebhookStatus) -> Self { +impl From for api::IncomingWebhookEvent { + fn from(status: VoltWebhookBodyEventType) -> Self { match status { - VoltWebhookStatus::Completed | VoltWebhookStatus::Received => { - Self::PaymentIntentSuccess - } - VoltWebhookStatus::Failed | VoltWebhookStatus::NotReceived => { - Self::PaymentIntentFailure - } - VoltWebhookStatus::Pending => Self::PaymentIntentProcessing, + VoltWebhookBodyEventType::Payment(payment_data) => match payment_data.status { + VoltWebhookPaymentStatus::Completed | VoltWebhookPaymentStatus::Received => { + Self::PaymentIntentSuccess + } + VoltWebhookPaymentStatus::Failed | VoltWebhookPaymentStatus::NotReceived => { + Self::PaymentIntentFailure + } + VoltWebhookPaymentStatus::Pending => Self::PaymentIntentProcessing, + }, + VoltWebhookBodyEventType::Refund(refund_data) => match refund_data.status { + VoltWebhookRefundsStatus::RefundConfirmed => Self::RefundSuccess, + VoltWebhookRefundsStatus::RefundFailed => Self::RefundFailure, + }, } } } From bb096138b5937092badd02741fb869ee35e2e3cc Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Thu, 11 Jan 2024 17:58:29 +0530 Subject: [PATCH 320/443] feat(router): payment_method block (#3056) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: shashank_attarde --- crates/api_models/src/blocklist.rs | 41 ++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/payments.rs | 3 + crates/cards/src/validate.rs | 7 + crates/common_enums/src/enums.rs | 34 +- crates/data_models/src/errors.rs | 2 + crates/data_models/src/payments.rs | 1 + .../src/payments/payment_intent.rs | 5 + crates/diesel_models/src/blocklist.rs | 26 ++ .../src/blocklist_fingerprint.rs | 26 ++ crates/diesel_models/src/blocklist_lookup.rs | 20 + crates/diesel_models/src/enums.rs | 6 +- crates/diesel_models/src/lib.rs | 3 + crates/diesel_models/src/payment_intent.rs | 9 + crates/diesel_models/src/query.rs | 3 + crates/diesel_models/src/query/blocklist.rs | 83 ++++ .../src/query/blocklist_fingerprint.rs | 33 ++ .../src/query/blocklist_lookup.rs | 48 +++ crates/diesel_models/src/schema.rs | 49 +++ .../router/src/compatibility/stripe/errors.rs | 1 + crates/router/src/consts.rs | 3 + crates/router/src/core.rs | 1 + crates/router/src/core/admin.rs | 12 + crates/router/src/core/blocklist.rs | 41 ++ .../router/src/core/blocklist/transformers.rs | 13 + crates/router/src/core/blocklist/utils.rs | 359 ++++++++++++++++++ .../src/core/errors/api_error_response.rs | 2 + crates/router/src/core/errors/transformers.rs | 1 + crates/router/src/core/payments/helpers.rs | 3 + .../payments/operations/payment_confirm.rs | 216 +++++++++-- .../payments/operations/payment_create.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../router/src/core/payments/transformers.rs | 1 + crates/router/src/db.rs | 6 + crates/router/src/db/blocklist.rs | 203 ++++++++++ crates/router/src/db/blocklist_fingerprint.rs | 95 +++++ crates/router/src/db/blocklist_lookup.rs | 125 ++++++ crates/router/src/lib.rs | 3 +- crates/router/src/routes.rs | 7 +- crates/router/src/routes/app.rs | 19 + crates/router/src/routes/blocklist.rs | 81 ++++ crates/router/src/routes/lock_utils.rs | 5 + crates/router/src/types/storage.rs | 6 +- crates/router/src/types/storage/blocklist.rs | 1 + .../types/storage/blocklist_fingerprint.rs | 1 + .../src/types/storage/blocklist_lookup.rs | 1 + crates/router/src/utils/user/sample_data.rs | 1 + crates/router_env/src/logger/types.rs | 6 + crates/storage_impl/src/errors.rs | 3 + .../src/mock_db/payment_intent.rs | 1 + .../src/payments/payment_intent.rs | 7 + .../down.sql | 5 + .../up.sql | 19 + .../down.sql | 3 + .../up.sql | 13 + .../down.sql | 2 + .../up.sql | 2 + .../down.sql | 3 + .../up.sql | 9 + openapi/openapi_spec.json | 5 + 60 files changed, 1649 insertions(+), 38 deletions(-) create mode 100644 crates/api_models/src/blocklist.rs create mode 100644 crates/diesel_models/src/blocklist.rs create mode 100644 crates/diesel_models/src/blocklist_fingerprint.rs create mode 100644 crates/diesel_models/src/blocklist_lookup.rs create mode 100644 crates/diesel_models/src/query/blocklist.rs create mode 100644 crates/diesel_models/src/query/blocklist_fingerprint.rs create mode 100644 crates/diesel_models/src/query/blocklist_lookup.rs create mode 100644 crates/router/src/core/blocklist.rs create mode 100644 crates/router/src/core/blocklist/transformers.rs create mode 100644 crates/router/src/core/blocklist/utils.rs create mode 100644 crates/router/src/db/blocklist.rs create mode 100644 crates/router/src/db/blocklist_fingerprint.rs create mode 100644 crates/router/src/db/blocklist_lookup.rs create mode 100644 crates/router/src/routes/blocklist.rs create mode 100644 crates/router/src/types/storage/blocklist.rs create mode 100644 crates/router/src/types/storage/blocklist_fingerprint.rs create mode 100644 crates/router/src/types/storage/blocklist_lookup.rs create mode 100644 migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql create mode 100644 migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql create mode 100644 migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql create mode 100644 migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql create mode 100644 migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql create mode 100644 migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql create mode 100644 migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql create mode 100644 migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql diff --git a/crates/api_models/src/blocklist.rs b/crates/api_models/src/blocklist.rs new file mode 100644 index 000000000000..fc838eed5ce6 --- /dev/null +++ b/crates/api_models/src/blocklist.rs @@ -0,0 +1,41 @@ +use common_enums::enums; +use common_utils::events::ApiEventMetric; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum BlocklistRequest { + CardBin(String), + Fingerprint(String), + ExtendedCardBin(String), +} + +pub type AddToBlocklistRequest = BlocklistRequest; +pub type DeleteFromBlocklistRequest = BlocklistRequest; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BlocklistResponse { + pub fingerprint_id: String, + pub data_kind: enums::BlocklistDataKind, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: time::PrimitiveDateTime, +} + +pub type AddToBlocklistResponse = BlocklistResponse; +pub type DeleteFromBlocklistResponse = BlocklistResponse; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ListBlocklistQuery { + pub data_kind: enums::BlocklistDataKind, + #[serde(default = "default_list_limit")] + pub limit: u16, + #[serde(default)] + pub offset: u16, +} + +fn default_list_limit() -> u16 { + 10 +} + +impl ApiEventMetric for BlocklistRequest {} +impl ApiEventMetric for BlocklistResponse {} +impl ApiEventMetric for ListBlocklistQuery {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 459443747e36..dc1f6eb65375 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -3,6 +3,7 @@ pub mod admin; pub mod analytics; pub mod api_keys; pub mod bank_accounts; +pub mod blocklist; pub mod cards_info; pub mod conditional_configs; pub mod connector_onboarding; diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 45611a91458f..f9077500dd4f 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2274,6 +2274,9 @@ pub struct PaymentsResponse { /// List of incremental authorizations happened to the payment pub incremental_authorizations: Option>, + + /// Payment Fingerprint + pub fingerprint: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index ca47c73c7c2c..87b04baa1a2c 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -24,6 +24,13 @@ impl CardNumber { pub fn get_card_isin(self) -> String { self.0.peek().chars().take(6).collect::() } + + pub fn get_extended_card_bin(self) -> String { + self.0.peek().chars().take(8).collect::() + } + pub fn get_card_no(self) -> String { + self.0.peek().chars().collect::() + } pub fn get_last4(self) -> String { self.0 .peek() diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 3af1c0e826be..949cc2e0034d 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -6,12 +6,13 @@ use utoipa::ToSchema; pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, - DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventType as EventType, - DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, - DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, - DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventType as EventType, DbFutureUsage as FutureUsage, + DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, + DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, + DbRefundStatus as RefundStatus, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, }; } @@ -275,6 +276,27 @@ pub enum AuthorizationStatus { Unresolved, } +#[derive( + Clone, + Debug, + PartialEq, + Eq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BlocklistDataKind { + PaymentMethod, + CardBin, + ExtendedCardBin, +} + #[derive( Clone, Copy, diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 9616a3a944ca..bed1ab9ccbf5 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -24,6 +24,8 @@ pub enum StorageError { SerializationFailed, #[error("MockDb error")] MockDbError, + #[error("Kafka error")] + KafkaError, #[error("Customer with this id is Redacted")] CustomerRedacted, #[error("Deserialization failure")] diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index cc6b03f89a5b..713003d666b2 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -53,5 +53,6 @@ pub struct PaymentIntent { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 80671ec7f61d..7470b5f85028 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -110,6 +110,7 @@ pub struct PaymentIntentNew { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } @@ -163,6 +164,7 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + fingerprint_id: Option, session_expiry: Option, }, PaymentAttemptAndAttemptCountUpdate { @@ -228,6 +230,7 @@ pub struct PaymentIntentUpdateInternal { pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } @@ -252,6 +255,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, } => Self { amount: Some(amount), @@ -272,6 +276,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, ..Default::default() }, diff --git a/crates/diesel_models/src/blocklist.rs b/crates/diesel_models/src/blocklist.rs new file mode 100644 index 000000000000..9e88802aa3bb --- /dev/null +++ b/crates/diesel_models/src/blocklist.rs @@ -0,0 +1,26 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist; + +#[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist)] +pub struct BlocklistNew { + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub metadata: Option, + pub created_at: time::PrimitiveDateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Deserialize, Serialize)] +#[diesel(table_name = blocklist)] +pub struct Blocklist { + #[serde(skip)] + pub id: i32, + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub metadata: Option, + pub created_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/blocklist_fingerprint.rs b/crates/diesel_models/src/blocklist_fingerprint.rs new file mode 100644 index 000000000000..e75856622e2f --- /dev/null +++ b/crates/diesel_models/src/blocklist_fingerprint.rs @@ -0,0 +1,26 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist_fingerprint; + +#[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist_fingerprint)] +pub struct BlocklistFingerprintNew { + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub encrypted_fingerprint: String, + pub created_at: time::PrimitiveDateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Queryable, Identifiable, Deserialize, Serialize)] +#[diesel(table_name = blocklist_fingerprint)] +pub struct BlocklistFingerprint { + #[serde(skip_serializing)] + pub id: i32, + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub encrypted_fingerprint: String, + pub created_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/blocklist_lookup.rs b/crates/diesel_models/src/blocklist_lookup.rs new file mode 100644 index 000000000000..ad2a893e03d9 --- /dev/null +++ b/crates/diesel_models/src/blocklist_lookup.rs @@ -0,0 +1,20 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist_lookup; + +#[derive(Default, Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist_lookup)] +pub struct BlocklistLookupNew { + pub merchant_id: String, + pub fingerprint: String, +} + +#[derive(Default, Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Deserialize, Serialize)] +#[diesel(table_name = blocklist_lookup)] +pub struct BlocklistLookup { + #[serde(skip)] + pub id: i32, + pub merchant_id: String, + pub fingerprint: String, +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 792e8ffc8bb3..a06937c99a6d 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -2,9 +2,9 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, DbConnectorStatus as ConnectorStatus, + DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDashboardMetadata as DashboardMetadata, DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index fa32fb84a15d..82b1e29ee838 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -1,11 +1,14 @@ pub mod address; pub mod api_keys; +pub mod blocklist_lookup; pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; pub mod customers; pub mod dispute; pub mod encryption; diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 17784bc56598..31bc0c06c51d 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -56,6 +56,7 @@ pub struct PaymentIntent { pub incremental_authorization_allowed: Option, pub authorization_count: Option, pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive( @@ -107,6 +108,7 @@ pub struct PaymentIntentNew { pub authorization_count: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -160,6 +162,7 @@ pub enum PaymentIntentUpdate { payment_confirm_source: Option, updated_by: String, session_expiry: Option, + fingerprint_id: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -226,6 +229,7 @@ pub struct PaymentIntentUpdateInternal { pub incremental_authorization_allowed: Option, pub authorization_count: Option, pub session_expiry: Option, + pub fingerprint_id: Option, } impl PaymentIntentUpdate { @@ -259,6 +263,7 @@ impl PaymentIntentUpdate { incremental_authorization_allowed, authorization_count, session_expiry, + fingerprint_id, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -288,9 +293,11 @@ impl PaymentIntentUpdate { payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), updated_by, surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), + incremental_authorization_allowed: incremental_authorization_allowed .or(source.incremental_authorization_allowed), authorization_count: authorization_count.or(source.authorization_count), + fingerprint_id: fingerprint_id.or(source.fingerprint_id), session_expiry: session_expiry.or(source.session_expiry), ..source } @@ -319,6 +326,7 @@ impl From for PaymentIntentUpdateInternal { payment_confirm_source, updated_by, session_expiry, + fingerprint_id, } => Self { amount: Some(amount), currency: Some(currency), @@ -339,6 +347,7 @@ impl From for PaymentIntentUpdateInternal { payment_confirm_source, updated_by, session_expiry, + fingerprint_id, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index 3a3dee47a854..3a0a008b76bd 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -1,11 +1,14 @@ pub mod address; pub mod api_keys; +pub mod blocklist_lookup; pub mod business_profile; mod capture; pub mod cards_info; pub mod configs; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; pub mod customers; pub mod dashboard_metadata; pub mod dispute; diff --git a/crates/diesel_models/src/query/blocklist.rs b/crates/diesel_models/src/query/blocklist.rs new file mode 100644 index 000000000000..e1ba5fa923d6 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist.rs @@ -0,0 +1,83 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist::{Blocklist, BlocklistNew}, + schema::blocklist::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Blocklist { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn list_by_merchant_id_data_kind( + conn: &PgPooledConn, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::data_kind.eq(data_kind.to_owned())), + Some(limit), + Some(offset), + Some(dsl::created_at.desc()), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn list_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + None, + None, + Some(dsl::created_at.desc()), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn delete_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_delete_one_with_result::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist_fingerprint.rs b/crates/diesel_models/src/query/blocklist_fingerprint.rs new file mode 100644 index 000000000000..4f3d77e63a81 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist_fingerprint.rs @@ -0,0 +1,33 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist_fingerprint::{BlocklistFingerprint, BlocklistFingerprintNew}, + schema::blocklist_fingerprint::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistFingerprintNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BlocklistFingerprint { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist_lookup.rs b/crates/diesel_models/src/query/blocklist_lookup.rs new file mode 100644 index 000000000000..ea28c94e4916 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist_lookup.rs @@ -0,0 +1,48 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist_lookup::{BlocklistLookup, BlocklistLookupNew}, + schema::blocklist_lookup::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistLookupNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BlocklistLookup { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint.eq(fingerprint.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn delete_by_merchant_id_fingerprint( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint: &str, + ) -> StorageResult { + generics::generic_delete_one_with_result::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint.eq(fingerprint.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index b29a362e3b02..131d2b182661 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -57,6 +57,50 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + fingerprint_id -> Varchar, + data_kind -> BlocklistDataKind, + metadata -> Nullable, + created_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist_fingerprint (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + fingerprint_id -> Varchar, + data_kind -> BlocklistDataKind, + encrypted_fingerprint -> Text, + created_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist_lookup (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + fingerprint -> Text, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -709,6 +753,8 @@ diesel::table! { incremental_authorization_allowed -> Nullable, authorization_count -> Nullable, session_expiry -> Nullable, + #[max_length = 64] + fingerprint_id -> Nullable, } } @@ -1016,6 +1062,9 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + blocklist, + blocklist_fingerprint, + blocklist_lookup, business_profile, captures, cards_info, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 5963110c6324..63205ea68ca6 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -520,6 +520,7 @@ impl From for StripeErrorCode { connector_name, }, errors::ApiErrorResponse::DuplicatePaymentMethod => Self::DuplicatePaymentMethod, + errors::ApiErrorResponse::PaymentBlocked => Self::PaymentFailed, errors::ApiErrorResponse::ClientSecretInvalid => Self::PaymentIntentInvalidParameter { param: "client_secret".to_owned(), }, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index afe761846304..ed020b0c7e0f 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,6 +27,9 @@ pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; /// Payment intent default client secret expiry (in seconds) pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; +/// The length of a merchant fingerprint secret +pub const FINGERPRINT_SECRET_LENGTH: usize = 64; + // String literals pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 0bd197ee22e9..5ae4b0be33da 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod api_keys; pub mod api_locking; +pub mod blocklist; pub mod cache; pub mod cards_info; pub mod conditional_config; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 2577bb83a3a2..e8593581126a 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -10,6 +10,7 @@ use common_utils::{ ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, }; +use diesel_models::configs; use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; @@ -141,6 +142,17 @@ pub async fn create_merchant_account( .transpose()? .map(Secret::new); + let fingerprint = Some(utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs")); + if let Some(fingerprint) = fingerprint { + db.insert_config(configs::ConfigNew { + key: format!("fingerprint_secret_{}", req.merchant_id), + config: fingerprint, + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Mot able to generate Merchant fingerprint")?; + }; + let organization_id = if let Some(organization_id) = req.organization_id.as_ref() { db.find_organization_by_org_id(organization_id) .await diff --git a/crates/router/src/core/blocklist.rs b/crates/router/src/core/blocklist.rs new file mode 100644 index 000000000000..85845602449c --- /dev/null +++ b/crates/router/src/core/blocklist.rs @@ -0,0 +1,41 @@ +pub mod transformers; +pub mod utils; + +use api_models::blocklist as api_blocklist; + +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services, + types::domain, +}; + +pub async fn add_entry_to_blocklist( + state: AppState, + merchant_account: domain::MerchantAccount, + body: api_blocklist::AddToBlocklistRequest, +) -> RouterResponse { + utils::insert_entry_into_blocklist(&state, merchant_account.merchant_id, body) + .await + .map(services::ApplicationResponse::Json) +} + +pub async fn remove_entry_from_blocklist( + state: AppState, + merchant_account: domain::MerchantAccount, + body: api_blocklist::DeleteFromBlocklistRequest, +) -> RouterResponse { + utils::delete_entry_from_blocklist(&state, merchant_account.merchant_id, body) + .await + .map(services::ApplicationResponse::Json) +} + +pub async fn list_blocklist_entries( + state: AppState, + merchant_account: domain::MerchantAccount, + query: api_blocklist::ListBlocklistQuery, +) -> RouterResponse> { + utils::list_blocklist_entries_for_merchant(&state, merchant_account.merchant_id, query) + .await + .map(services::ApplicationResponse::Json) +} diff --git a/crates/router/src/core/blocklist/transformers.rs b/crates/router/src/core/blocklist/transformers.rs new file mode 100644 index 000000000000..2cb5f86a264a --- /dev/null +++ b/crates/router/src/core/blocklist/transformers.rs @@ -0,0 +1,13 @@ +use api_models::blocklist; + +use crate::types::{storage, transformers::ForeignFrom}; + +impl ForeignFrom for blocklist::AddToBlocklistResponse { + fn foreign_from(from: storage::Blocklist) -> Self { + Self { + fingerprint_id: from.fingerprint_id, + data_kind: from.data_kind, + created_at: from.created_at, + } + } +} diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs new file mode 100644 index 000000000000..b7effaf63acf --- /dev/null +++ b/crates/router/src/core/blocklist/utils.rs @@ -0,0 +1,359 @@ +use api_models::blocklist as api_blocklist; +use common_utils::crypto::{self, SignMessage}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; + +use super::{errors, AppState}; +use crate::{ + consts, + core::errors::{RouterResult, StorageErrorExt}, + types::{storage, transformers::ForeignInto}, + utils, +}; + +pub async fn delete_entry_from_blocklist( + state: &AppState, + merchant_id: String, + request: api_blocklist::DeleteFromBlocklistRequest, +) -> RouterResult { + let blocklist_entry = match request { + api_blocklist::DeleteFromBlocklistRequest::CardBin(bin) => { + delete_card_bin_blocklist_entry(state, &bin, &merchant_id).await? + } + + api_blocklist::DeleteFromBlocklistRequest::ExtendedCardBin(xbin) => { + delete_card_bin_blocklist_entry(state, &xbin, &merchant_id).await? + } + + api_blocklist::DeleteFromBlocklistRequest::Fingerprint(fingerprint_id) => { + let blocklist_fingerprint = state + .store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &merchant_id, + &fingerprint_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "blocklist record with given fingerprint id not found".to_string(), + })?; + + #[cfg(feature = "kms")] + let decrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(blocklist_fingerprint.encrypted_fingerprint) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to kms decrypt fingerprint")?; + + #[cfg(not(feature = "kms"))] + let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint; + + let blocklist_entry = state + .store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, &fingerprint_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist record for the given fingerprint id was found" + .to_string(), + })?; + + state + .store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + &decrypted_fingerprint, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist record for the given fingerprint id was found" + .to_string(), + })?; + + blocklist_entry + } + }; + + Ok(blocklist_entry.foreign_into()) +} + +pub async fn list_blocklist_entries_for_merchant( + state: &AppState, + merchant_id: String, + query: api_blocklist::ListBlocklistQuery, +) -> RouterResult> { + state + .store + .list_blocklist_entries_by_merchant_id_data_kind( + &merchant_id, + query.data_kind, + query.limit.into(), + query.offset.into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist records found".to_string(), + }) + .map(|v| v.into_iter().map(ForeignInto::foreign_into).collect()) +} + +fn validate_card_bin(bin: &str) -> RouterResult<()> { + if bin.len() == 6 && bin.chars().all(|c| c.is_ascii_digit()) { + Ok(()) + } else { + Err(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "data".to_string(), + expected_format: "a 6 digit number".to_string(), + }) + .into_report() + } +} + +fn validate_extended_card_bin(bin: &str) -> RouterResult<()> { + if bin.len() == 8 && bin.chars().all(|c| c.is_ascii_digit()) { + Ok(()) + } else { + Err(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "data".to_string(), + expected_format: "an 8 digit number".to_string(), + }) + .into_report() + } +} + +pub async fn insert_entry_into_blocklist( + state: &AppState, + merchant_id: String, + to_block: api_blocklist::AddToBlocklistRequest, +) -> RouterResult { + let blocklist_entry = match &to_block { + api_blocklist::AddToBlocklistRequest::CardBin(bin) => { + validate_card_bin(bin)?; + duplicate_check_insert_bin( + bin, + state, + &merchant_id, + common_enums::BlocklistDataKind::CardBin, + ) + .await? + } + + api_blocklist::AddToBlocklistRequest::ExtendedCardBin(bin) => { + validate_extended_card_bin(bin)?; + duplicate_check_insert_bin( + bin, + state, + &merchant_id, + common_enums::BlocklistDataKind::ExtendedCardBin, + ) + .await? + } + + api_blocklist::AddToBlocklistRequest::Fingerprint(fingerprint_id) => { + let blocklist_entry_result = state + .store + .find_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, fingerprint_id) + .await; + + match blocklist_entry_result { + Ok(_) => { + return Err(errors::ApiErrorResponse::PreconditionFailed { + message: "data associated with the given fingerprint is already blocked" + .to_string(), + }) + .into_report(); + } + + // if it is a db not found error, we can proceed as normal + Err(inner) if inner.current_context().is_db_not_found() => {} + + err @ Err(_) => { + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching blocklist entry from table")?; + } + } + + let blocklist_fingerprint = state + .store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &merchant_id, + fingerprint_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "fingerprint not found".to_string(), + })?; + + #[cfg(feature = "kms")] + let decrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(blocklist_fingerprint.encrypted_fingerprint) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to kms decrypt encrypted fingerprint")?; + + #[cfg(not(feature = "kms"))] + let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint; + + state + .store + .insert_blocklist_lookup_entry( + diesel_models::blocklist_lookup::BlocklistLookupNew { + merchant_id: merchant_id.clone(), + fingerprint: decrypted_fingerprint, + }, + ) + .await + .to_duplicate_response(errors::ApiErrorResponse::PreconditionFailed { + message: "the payment instrument associated with the given fingerprint is already in the blocklist".to_string(), + }) + .attach_printable("failed to add fingerprint to blocklist lookup")?; + + state + .store + .insert_blocklist_entry(storage::BlocklistNew { + merchant_id: merchant_id.clone(), + fingerprint_id: fingerprint_id.clone(), + data_kind: blocklist_fingerprint.data_kind, + metadata: None, + created_at: common_utils::date_time::now(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to add fingerprint to pm blocklist")? + } + }; + + Ok(blocklist_entry.foreign_into()) +} + +pub async fn get_merchant_fingerprint_secret( + state: &AppState, + merchant_id: &str, +) -> RouterResult { + let key = get_merchant_fingerprint_secret_key(merchant_id); + let config_fetch_result = state.store.find_config_by_key(&key).await; + + match config_fetch_result { + Ok(config) => Ok(config.config), + + Err(e) if e.current_context().is_db_not_found() => { + let new_fingerprint_secret = + utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs"); + let new_config = storage::ConfigNew { + key, + config: new_fingerprint_secret.clone(), + }; + + state + .store + .insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to create new fingerprint secret for merchant")?; + + Ok(new_fingerprint_secret) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching merchant fingerprint secret"), + } +} + +pub fn get_merchant_fingerprint_secret_key(merchant_id: &str) -> String { + format!("fingerprint_secret_{merchant_id}") +} + +async fn duplicate_check_insert_bin( + bin: &str, + state: &AppState, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, +) -> RouterResult { + let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; + let bin_fingerprint = crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_secret.clone().as_bytes(), + bin.as_bytes(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error in bin hash creation")?; + + let encoded_fingerprint = hex::encode(bin_fingerprint.clone()); + + let blocklist_entry_result = state + .store + .find_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin) + .await; + + match blocklist_entry_result { + Ok(_) => { + return Err(errors::ApiErrorResponse::PreconditionFailed { + message: "provided bin is already blocked".to_string(), + }) + .into_report(); + } + + Err(e) if e.current_context().is_db_not_found() => {} + + err @ Err(_) => { + return err + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to fetch blocklist entry"); + } + } + + // Checking for duplicacy + state + .store + .insert_blocklist_lookup_entry(diesel_models::blocklist_lookup::BlocklistLookupNew { + merchant_id: merchant_id.to_string(), + fingerprint: encoded_fingerprint.clone(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting blocklist lookup entry")?; + + state + .store + .insert_blocklist_entry(storage::BlocklistNew { + merchant_id: merchant_id.to_string(), + fingerprint_id: bin.to_string(), + data_kind, + metadata: None, + created_at: common_utils::date_time::now(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting pm blocklist item") +} + +async fn delete_card_bin_blocklist_entry( + state: &AppState, + bin: &str, + merchant_id: &str, +) -> RouterResult { + let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; + let bin_fingerprint = crypto::HmacSha512 + .sign_message(merchant_secret.as_bytes(), bin.as_bytes()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error when hashing card bin")?; + let encoded_fingerprint = hex::encode(bin_fingerprint); + + state + .store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, &encoded_fingerprint) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "could not find a blocklist entry for the given bin".to_string(), + })?; + + state + .store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "could not find a blocklist entry for the given bin".to_string(), + }) +} diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index f94504cf274d..54ec4ec1e295 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -186,6 +186,8 @@ pub enum ApiErrorResponse { PaymentNotSucceeded, #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "The specified merchant connector account is disabled")] MerchantConnectorAccountDisabled, + #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "The specified payment is blocked")] + PaymentBlocked, #[error(error_type= ErrorType::ObjectNotFound, code = "HE_04", message = "Successful payment not found for the given payment id")] SuccessfulPaymentNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "The connector provided in the request is incorrect or not available")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index fa9a5185790d..ff764cafed62 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -187,6 +187,7 @@ impl ErrorSwitch for ApiErrorRespon AER::BadRequest(ApiError::new("HE", 3, "Mandate Validation Failed", Some(Extra { reason: Some(reason.clone()), ..Default::default() }))) } Self::PaymentNotSucceeded => AER::BadRequest(ApiError::new("HE", 3, "The payment has not succeeded yet. Please pass a successful payment to initiate refund", None)), + Self::PaymentBlocked => AER::BadRequest(ApiError::new("HE", 3, "The payment is blocked", None)), Self::SuccessfulPaymentNotFound => { AER::NotFound(ApiError::new("HE", 4, "Successful payment not found for the given payment id", None)) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index ec6371f310f2..003c09b73817 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2586,6 +2586,7 @@ mod tests { modified_at: common_utils::date_time::now(), last_synced: None, setup_future_usage: None, + fingerprint_id: None, off_session: None, client_secret: Some("1".to_string()), active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()), @@ -2638,6 +2639,7 @@ mod tests { statement_descriptor_suffix: None, created_at: common_utils::date_time::now().saturating_sub(time::Duration::seconds(20)), modified_at: common_utils::date_time::now(), + fingerprint_id: None, last_synced: None, setup_future_usage: None, off_session: None, @@ -2695,6 +2697,7 @@ mod tests { setup_future_usage: None, off_session: None, client_secret: None, + fingerprint_id: None, active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()), business_country: None, business_label: None, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 00ae8da6ae49..c81145c5de72 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -2,23 +2,30 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use common_utils::ext_traits::{AsyncExt, Encode}; +use common_utils::{ + crypto::{self, SignMessage}, + ext_traits::{AsyncExt, Encode}, +}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; use futures::FutureExt; use router_derive::PaymentOperation; -use router_env::{instrument, tracing}; +use router_env::{instrument, logger, tracing}; use tracing_futures::Instrument; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ + consts, core::{ + blocklist::utils as blocklist_utils, errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils}, + utils as core_utils, }, db::StorageInterface, routes::AppState, @@ -620,32 +627,34 @@ impl where F: 'b + Send, { + let db = state.store.as_ref(); let payment_method = payment_data.payment_attempt.payment_method; let browser_info = payment_data.payment_attempt.browser_info.clone(); let frm_message = payment_data.frm_message.clone(); - let (intent_status, attempt_status, (error_code, error_message)) = match frm_suggestion { - Some(FrmSuggestion::FrmCancelTransaction) => ( - storage_enums::IntentStatus::Failed, - storage_enums::AttemptStatus::Failure, - frm_message.map_or((None, None), |fraud_check| { - ( - Some(Some(fraud_check.frm_status.to_string())), - Some(fraud_check.frm_reason.map(|reason| reason.to_string())), - ) - }), - ), - Some(FrmSuggestion::FrmManualReview) => ( - storage_enums::IntentStatus::RequiresMerchantAction, - storage_enums::AttemptStatus::Unresolved, - (None, None), - ), - _ => ( - storage_enums::IntentStatus::Processing, - storage_enums::AttemptStatus::Pending, - (None, None), - ), - }; + let (mut intent_status, mut attempt_status, (error_code, error_message)) = + match frm_suggestion { + Some(FrmSuggestion::FrmCancelTransaction) => ( + storage_enums::IntentStatus::Failed, + storage_enums::AttemptStatus::Failure, + frm_message.map_or((None, None), |fraud_check| { + ( + Some(Some(fraud_check.frm_status.to_string())), + Some(fraud_check.frm_reason.map(|reason| reason.to_string())), + ) + }), + ), + Some(FrmSuggestion::FrmManualReview) => ( + storage_enums::IntentStatus::RequiresMerchantAction, + storage_enums::AttemptStatus::Unresolved, + (None, None), + ), + _ => ( + storage_enums::IntentStatus::Processing, + storage_enums::AttemptStatus::Pending, + (None, None), + ), + }; let connector = payment_data.payment_attempt.connector.clone(); let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); @@ -709,6 +718,157 @@ impl let m_error_message = error_message.clone(); let m_db = state.clone().store; + // Validate Blocklist + let merchant_id = payment_data.payment_attempt.merchant_id; + let merchant_fingerprint_secret = + blocklist_utils::get_merchant_fingerprint_secret(state, &merchant_id).await?; + + // Hashed Fingerprint to check whether or not this payment should be blocked. + let card_number_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_no().as_bytes(), + ) + .attach_printable("error in pm fingerprint creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + // Hashed Cardbin to check whether or not this payment should be blocked. + let card_bin_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_isin().as_bytes(), + ) + .attach_printable("error in card bin hash creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + // Hashed Extended Cardbin to check whether or not this payment should be blocked. + let extended_card_bin_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_extended_card_bin().as_bytes(), + ) + .attach_printable("error in extended card bin hash creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + let mut fingerprint_id = None; + + //validating the payment method. + let mut is_pm_blocklisted = false; + + let mut blocklist_futures = Vec::new(); + if let Some(card_number_fingerprint) = card_number_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + card_number_fingerprint, + )); + } + + if let Some(card_bin_fingerprint) = card_bin_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + card_bin_fingerprint, + )); + } + + if let Some(extended_card_bin_fingerprint) = extended_card_bin_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + extended_card_bin_fingerprint, + )); + } + + let blocklist_lookups = futures::future::join_all(blocklist_futures).await; + + if blocklist_lookups.iter().any(|x| x.is_ok()) { + intent_status = storage_enums::IntentStatus::Failed; + attempt_status = storage_enums::AttemptStatus::Failure; + is_pm_blocklisted = true; + } + + if let Some(encoded_hash) = card_number_fingerprint { + #[cfg(feature = "kms")] + let encrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .encrypt(encoded_hash) + .await + .map_or_else( + |e| { + logger::error!(error=?e, "failed kms encryption of card fingerprint"); + None + }, + Some, + ); + + #[cfg(not(feature = "kms"))] + let encrypted_fingerprint = Some(encoded_hash); + + if let Some(encrypted_fingerprint) = encrypted_fingerprint { + fingerprint_id = db + .insert_blocklist_fingerprint_entry( + diesel_models::blocklist_fingerprint::BlocklistFingerprintNew { + merchant_id, + fingerprint_id: utils::generate_id(consts::ID_LENGTH, "fingerprint"), + encrypted_fingerprint, + data_kind: common_enums::BlocklistDataKind::PaymentMethod, + created_at: common_utils::date_time::now(), + }, + ) + .await + .map_or_else( + |e| { + logger::error!(error=?e, "failed storing card fingerprint in db"); + None + }, + |fp| Some(fp.fingerprint_id), + ); + } + } + let surcharge_amount = payment_data .surcharge_details .as_ref() @@ -789,6 +949,7 @@ impl metadata: m_metadata, payment_confirm_source: header_payload.payment_confirm_source, updated_by: m_storage_scheme, + fingerprint_id, session_expiry, }, storage_scheme, @@ -838,6 +999,11 @@ impl payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; + // Block the payment if the entry was present in the Blocklist + if is_pm_blocklisted { + return Err(errors::ApiErrorResponse::PaymentBlocked.into()); + } + Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 09ec436ed001..2b25a74deb19 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -825,6 +825,7 @@ impl PaymentCreate { request_incremental_authorization, incremental_authorization_allowed: None, authorization_count: None, + fingerprint_id: None, session_expiry: Some(session_expiry), }) } diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index afb83d38dc5e..e002b92d1810 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -617,6 +617,7 @@ impl metadata, payment_confirm_source: None, updated_by: storage_scheme.to_string(), + fingerprint_id: None, session_expiry, }, storage_scheme, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 359373e469b7..5a3a322fb14d 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -706,6 +706,7 @@ where .set_incremental_authorization_allowed( payment_intent.incremental_authorization_allowed, ) + .set_fingerprint(payment_intent.fingerprint_id) .set_authorization_count(payment_intent.authorization_count) .set_incremental_authorizations(incremental_authorizations_response) .to_owned(), diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 5beace9cbb83..b9d346b7a71f 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,6 +1,9 @@ pub mod address; pub mod api_keys; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; +pub mod blocklist_lookup; pub mod business_profile; pub mod cache; pub mod capture; @@ -68,6 +71,7 @@ pub trait StorageInterface: + dyn_clone::DynClone + address::AddressInterface + api_keys::ApiKeyInterface + + blocklist_lookup::BlocklistLookupInterface + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface @@ -85,6 +89,8 @@ pub trait StorageInterface: + PaymentAttemptInterface + PaymentIntentInterface + payment_method::PaymentMethodInterface + + blocklist::BlocklistInterface + + blocklist_fingerprint::BlocklistFingerprintInterface + scheduler::SchedulerInterface + payout_attempt::PayoutAttemptInterface + payouts::PayoutsInterface diff --git a/crates/router/src/db/blocklist.rs b/crates/router/src/db/blocklist.rs new file mode 100644 index 000000000000..c263bef63c5a --- /dev/null +++ b/crates/router/src/db/blocklist.rs @@ -0,0 +1,203 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistInterface { + async fn insert_blocklist_entry( + &self, + pm_blocklist_new: storage::BlocklistNew, + ) -> CustomResult; + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl BlocklistInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + pm_blocklist + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::find_by_merchant_id_fingerprint_id(&conn, merchant_id, fingerprint_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::list_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::list_by_merchant_id_data_kind( + &conn, + merchant_id, + data_kind, + limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::delete_by_merchant_id_fingerprint_id(&conn, merchant_id, fingerprint_id) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + _pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + _merchant_id: &str, + _data_kind: common_enums::BlocklistDataKind, + _limit: i64, + _offset: i64, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + _pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + _merchant_id: &str, + _data_kind: common_enums::BlocklistDataKind, + _limit: i64, + _offset: i64, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::KafkaError)? + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::KafkaError)? + } +} diff --git a/crates/router/src/db/blocklist_fingerprint.rs b/crates/router/src/db/blocklist_fingerprint.rs new file mode 100644 index 000000000000..9da7c7d8fb2c --- /dev/null +++ b/crates/router/src/db/blocklist_fingerprint.rs @@ -0,0 +1,95 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistFingerprintInterface { + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult; + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + pm_fingerprint_new + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistFingerprint::find_by_merchant_id_fingerprint_id( + &conn, + merchant_id, + fingerprint_id, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + _pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + _pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } +} diff --git a/crates/router/src/db/blocklist_lookup.rs b/crates/router/src/db/blocklist_lookup.rs new file mode 100644 index 000000000000..0dfd81c8b8a2 --- /dev/null +++ b/crates/router/src/db/blocklist_lookup.rs @@ -0,0 +1,125 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistLookupInterface { + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_new: storage::BlocklistLookupNew, + ) -> CustomResult; + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult; + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + blocklist_lookup_entry + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistLookup::find_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistLookup::delete_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + _blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + _blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 3b4c7ce9b7d3..696198f2153c 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -129,9 +129,9 @@ pub fn mk_app( #[cfg(feature = "oltp")] { server_app = server_app - .service(routes::PaymentMethods::server(state.clone())) .service(routes::EphemeralKey::server(state.clone())) .service(routes::Webhooks::server(state.clone())) + .service(routes::PaymentMethods::server(state.clone())) } #[cfg(feature = "olap")] @@ -143,6 +143,7 @@ pub fn mk_app( .service(routes::Disputes::server(state.clone())) .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::Blocklist::server(state.clone())) .service(routes::LockerMigrate::server(state.clone())) .service(routes::Gsm::server(state.clone())) .service(routes::PaymentLink::server(state.clone())) diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ec718b2dde9f..d4bfabb6f92a 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -1,6 +1,8 @@ pub mod admin; pub mod api_keys; pub mod app; +#[cfg(feature = "olap")] +pub mod blocklist; pub mod cache; pub mod cards_info; pub mod configs; @@ -42,14 +44,15 @@ pub mod webhooks; pub mod locker_migration; #[cfg(any(feature = "olap", feature = "oltp"))] pub mod pm_auth; +#[cfg(feature = "olap")] +pub use app::{Blocklist, Routing}; + #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(any(feature = "olap", feature = "oltp"))] pub use self::app::Forex; #[cfg(feature = "payouts")] pub use self::app::Payouts; -#[cfg(feature = "olap")] -pub use self::app::Routing; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 015e3305de10..0b2acaf4e506 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -14,6 +14,8 @@ use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; +#[cfg(feature = "olap")] +use super::blocklist; #[cfg(any(feature = "olap", feature = "oltp"))] use super::currency; #[cfg(feature = "dummy_connector")] @@ -566,6 +568,23 @@ impl PaymentMethods { } } +#[cfg(feature = "olap")] +pub struct Blocklist; + +#[cfg(feature = "olap")] +impl Blocklist { + pub fn server(state: AppState) -> Scope { + web::scope("/blocklist") + .app_data(web::Data::new(state)) + .service( + web::resource("") + .route(web::get().to(blocklist::list_blocked_payment_methods)) + .route(web::post().to(blocklist::add_entry_to_blocklist)) + .route(web::delete().to(blocklist::remove_entry_from_blocklist)), + ) + } +} + pub struct MerchantAccount; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/blocklist.rs b/crates/router/src/routes/blocklist.rs new file mode 100644 index 000000000000..7c268dddeec0 --- /dev/null +++ b/crates/router/src/routes/blocklist.rs @@ -0,0 +1,81 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::blocklist as api_blocklist; +use router_env::Flow; + +use crate::{ + core::{api_locking, blocklist}, + routes::AppState, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn add_entry_to_blocklist( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AddToBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, body| { + blocklist::add_entry_to_blocklist(state, auth.merchant_account, body) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn remove_entry_from_blocklist( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::DeleteFromBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, body| { + blocklist::remove_entry_from_blocklist(state, auth.merchant_account, body) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn list_blocked_payment_methods( + state: web::Data, + req: HttpRequest, + query_payload: web::Query, +) -> HttpResponse { + let flow = Flow::ListBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + query_payload.into_inner(), + |state, auth: auth::AuthenticationData, query| { + blocklist::list_blocklist_entries(state, auth.merchant_account, query) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 10f408f3d4f0..55c6cbc23d70 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -24,6 +24,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Blocklist, Forex, RustLockerMigration, Gsm, @@ -57,6 +58,10 @@ impl From for ApiIdentifier { Flow::RetrieveForexFlow => Self::Forex, + Flow::AddToBlocklist => Self::Blocklist, + Flow::DeleteFromBlocklist => Self::Blocklist, + Flow::ListBlocklist => Self::Blocklist, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 56d3272b9471..b93cbbbbba92 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,6 +1,9 @@ pub mod address; pub mod api_keys; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; +pub mod blocklist_lookup; pub mod business_profile; pub mod capture; pub mod cards_info; @@ -43,7 +46,8 @@ pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate} pub use scheduler::db::process_tracker; pub use self::{ - address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, + address::*, api_keys::*, authorization::*, blocklist::*, blocklist_fingerprint::*, + blocklist_lookup::*, capture::*, cards_info::*, configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, diff --git a/crates/router/src/types/storage/blocklist.rs b/crates/router/src/types/storage/blocklist.rs new file mode 100644 index 000000000000..7e7648dd4a08 --- /dev/null +++ b/crates/router/src/types/storage/blocklist.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist::{Blocklist, BlocklistNew}; diff --git a/crates/router/src/types/storage/blocklist_fingerprint.rs b/crates/router/src/types/storage/blocklist_fingerprint.rs new file mode 100644 index 000000000000..092d881e3fae --- /dev/null +++ b/crates/router/src/types/storage/blocklist_fingerprint.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist_fingerprint::{BlocklistFingerprint, BlocklistFingerprintNew}; diff --git a/crates/router/src/types/storage/blocklist_lookup.rs b/crates/router/src/types/storage/blocklist_lookup.rs new file mode 100644 index 000000000000..978708ff7c33 --- /dev/null +++ b/crates/router/src/types/storage/blocklist_lookup.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist_lookup::{BlocklistLookup, BlocklistLookupNew}; diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 33f1e2115349..dcf635595e0f 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -199,6 +199,7 @@ pub async fn generate_sample_data( request_incremental_authorization: Default::default(), incremental_authorization_allowed: Default::default(), authorization_count: Default::default(), + fingerprint_id: None, session_expiry: Some(session_expiry), }; let payment_attempt = PaymentAttemptBatchNew { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index e37e15443bdb..a6ac1b1e0a14 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -185,6 +185,12 @@ pub enum Flow { RoutingUpdateDefaultConfig, /// Routing delete config RoutingDeleteConfig, + /// Add record to blocklist + AddToBlocklist, + /// Delete record from blocklist + DeleteFromBlocklist, + /// List entries from blocklist + ListBlocklist, /// Incoming Webhook Receive IncomingWebhookReceive, /// Validate payment method flow diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 50173bb1c739..ac3a04e85b2b 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -55,6 +55,8 @@ pub enum StorageError { SerializationFailed, #[error("MockDb error")] MockDbError, + #[error("Kafka error")] + KafkaError, #[error("Customer with this id is Redacted")] CustomerRedacted, #[error("Deserialization failure")] @@ -103,6 +105,7 @@ impl Into for &StorageError { StorageError::KVError => DataStorageError::KVError, StorageError::SerializationFailed => DataStorageError::SerializationFailed, StorageError::MockDbError => DataStorageError::MockDbError, + StorageError::KafkaError => DataStorageError::KafkaError, StorageError::CustomerRedacted => DataStorageError::CustomerRedacted, StorageError::DeserializationFailed => DataStorageError::DeserializationFailed, StorageError::EncryptionError => DataStorageError::EncryptionError, diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index ee8676106f1d..3f892ed9fa7a 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -109,6 +109,7 @@ impl PaymentIntentInterface for MockDb { request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, authorization_count: new.authorization_count, + fingerprint_id: new.fingerprint_id, session_expiry: new.session_expiry, }; payment_intents.push(payment_intent.clone()); diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 07d70c9056b7..8d20dfe0f32b 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -101,6 +101,7 @@ impl PaymentIntentInterface for KVRouterStore { request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, authorization_count: new.authorization_count, + fingerprint_id: new.fingerprint_id.clone(), session_expiry: new.session_expiry, }; let redis_entry = kv::TypedSql { @@ -769,6 +770,7 @@ impl DataModelExt for PaymentIntentNew { request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, authorization_count: self.authorization_count, + fingerprint_id: self.fingerprint_id, session_expiry: self.session_expiry, } } @@ -813,6 +815,7 @@ impl DataModelExt for PaymentIntentNew { request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, session_expiry: storage_model.session_expiry, } } @@ -862,6 +865,7 @@ impl DataModelExt for PaymentIntent { request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, authorization_count: self.authorization_count, + fingerprint_id: self.fingerprint_id, session_expiry: self.session_expiry, } } @@ -907,6 +911,7 @@ impl DataModelExt for PaymentIntent { request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, session_expiry: storage_model.session_expiry, } } @@ -990,6 +995,7 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, } => DieselPaymentIntentUpdate::Update { amount, @@ -1009,6 +1015,7 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, }, Self::PaymentAttemptAndAttemptCountUpdate { diff --git a/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql b/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql new file mode 100644 index 000000000000..74c450622a7e --- /dev/null +++ b/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist_fingerprint; + +DROP TYPE "BlocklistDataKind"; diff --git a/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql b/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql new file mode 100644 index 000000000000..417d779200fc --- /dev/null +++ b/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here + +CREATE TYPE "BlocklistDataKind" AS ENUM ( + 'payment_method', + 'card_bin', + 'extended_card_bin' +); + +CREATE TABLE blocklist_fingerprint ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint_id VARCHAR(64) NOT NULL, + data_kind "BlocklistDataKind" NOT NULL, + encrypted_fingerprint TEXT NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX blocklist_fingerprint_merchant_id_fingerprint_id_index +ON blocklist_fingerprint (merchant_id, fingerprint_id); diff --git a/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql b/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql new file mode 100644 index 000000000000..cd7d412aad96 --- /dev/null +++ b/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist; diff --git a/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql b/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql new file mode 100644 index 000000000000..6d921dd78c30 --- /dev/null +++ b/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here + +CREATE TABLE blocklist ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint_id VARCHAR(64) NOT NULL, + data_kind "BlocklistDataKind" NOT NULL, + metadata JSONB, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX blocklist_unique_fingerprint_id_index ON blocklist (merchant_id, fingerprint_id); +CREATE INDEX blocklist_merchant_id_data_kind_created_at_index ON blocklist (merchant_id, data_kind, created_at DESC); diff --git a/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql new file mode 100644 index 000000000000..46b871b6ee4c --- /dev/null +++ b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS fingerprint_id; diff --git a/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql new file mode 100644 index 000000000000..831fb7b6ffc7 --- /dev/null +++ b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS fingerprint_id VARCHAR(64); diff --git a/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql b/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql new file mode 100644 index 000000000000..d2363f547a50 --- /dev/null +++ b/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist_lookup; diff --git a/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql b/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql new file mode 100644 index 000000000000..8af3e209fc62 --- /dev/null +++ b/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here + +CREATE TABLE blocklist_lookup ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint TEXT NOT NULL +); + +CREATE UNIQUE INDEX blocklist_lookup_merchant_id_fingerprint_index ON blocklist_lookup (merchant_id, fingerprint); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4423d1177c91..7a2b5504e0ec 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10726,6 +10726,11 @@ }, "description": "List of incremental authorizations happened to the payment", "nullable": true + }, + "fingerprint": { + "type": "string", + "description": "Payment Fingerprint", + "nullable": true } } }, From 54d44bef730c0679f3535f66e89e88139d70ba2e Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:42:09 +0530 Subject: [PATCH 321/443] feat(outgoingwebhookevent): adding api for query to fetch outgoing webhook events log (#3310) Co-authored-by: Sampras Lopes --- crates/analytics/src/clickhouse.rs | 19 ++++ crates/analytics/src/lib.rs | 1 + .../analytics/src/outgoing_webhook_event.rs | 6 ++ .../src/outgoing_webhook_event/core.rs | 27 ++++++ .../src/outgoing_webhook_event/events.rs | 90 +++++++++++++++++++ crates/analytics/src/sqlx.rs | 2 + crates/analytics/src/types.rs | 1 + crates/api_models/src/analytics.rs | 1 + .../src/analytics/outgoing_webhook_event.rs | 10 +++ crates/api_models/src/events.rs | 7 +- crates/router/src/analytics.rs | 30 ++++++- crates/router_env/src/lib.rs | 1 + 12 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 crates/analytics/src/outgoing_webhook_event.rs create mode 100644 crates/analytics/src/outgoing_webhook_event/core.rs create mode 100644 crates/analytics/src/outgoing_webhook_event/events.rs create mode 100644 crates/api_models/src/analytics/outgoing_webhook_event.rs diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index 964486c93649..b8fd5e6a35d0 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -21,6 +21,7 @@ use crate::{ filters::ApiEventFilter, metrics::{latency::LatencyAvg, ApiEventMetricRow}, }, + outgoing_webhook_event::events::OutgoingWebhookLogsResult, sdk_events::events::SdkEventsResult, types::TableEngine, }; @@ -120,6 +121,7 @@ impl AnalyticsDataSource for ClickhouseClient { } AnalyticsCollection::SdkEvents => TableEngine::BasicTree, AnalyticsCollection::ApiEvents => TableEngine::BasicTree, + AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, } } } @@ -145,6 +147,10 @@ impl super::sdk_events::events::SdkEventsFilterAnalytics for ClickhouseClient {} impl super::api_event::events::ApiLogsFilterAnalytics for ClickhouseClient {} impl super::api_event::filters::ApiEventFilterAnalytics for ClickhouseClient {} impl super::api_event::metrics::ApiEventMetricAnalytics for ClickhouseClient {} +impl super::outgoing_webhook_event::events::OutgoingWebhookLogsFilterAnalytics + for ClickhouseClient +{ +} #[derive(Debug, serde::Serialize)] struct CkhQuery { @@ -302,6 +308,18 @@ impl TryInto for serde_json::Value { } } +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse OutgoingWebhookLogsResult in clickhouse results", + )) + } +} + impl ToSql for PrimitiveDateTime { fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { let format = @@ -326,6 +344,7 @@ impl ToSql for AnalyticsCollection { Self::SdkEvents => Ok("sdk_events_dist".to_string()), Self::ApiEvents => Ok("api_audit_log".to_string()), Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), } } } diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 24da77f84f2b..8529807a1a16 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -7,6 +7,7 @@ mod query; pub mod refunds; pub mod api_event; +pub mod outgoing_webhook_event; pub mod sdk_events; mod sqlx; mod types; diff --git a/crates/analytics/src/outgoing_webhook_event.rs b/crates/analytics/src/outgoing_webhook_event.rs new file mode 100644 index 000000000000..9919d8bbb0fd --- /dev/null +++ b/crates/analytics/src/outgoing_webhook_event.rs @@ -0,0 +1,6 @@ +mod core; +pub mod events; + +pub trait OutgoingWebhookEventAnalytics: events::OutgoingWebhookLogsFilterAnalytics {} + +pub use self::core::outgoing_webhook_events_core; diff --git a/crates/analytics/src/outgoing_webhook_event/core.rs b/crates/analytics/src/outgoing_webhook_event/core.rs new file mode 100644 index 000000000000..5024cc70ec1c --- /dev/null +++ b/crates/analytics/src/outgoing_webhook_event/core.rs @@ -0,0 +1,27 @@ +use api_models::analytics::outgoing_webhook_event::OutgoingWebhookLogsRequest; +use common_utils::errors::ReportSwitchExt; +use error_stack::{IntoReport, ResultExt}; + +use super::events::{get_outgoing_webhook_event, OutgoingWebhookLogsResult}; +use crate::{errors::AnalyticsResult, types::FiltersError, AnalyticsProvider}; + +pub async fn outgoing_webhook_events_core( + pool: &AnalyticsProvider, + req: OutgoingWebhookLogsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( + "Outgoing Webhook Events Logs not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for Outgoing Webhook Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_outgoing_webhook_event(&merchant_id, req, ckh_pool).await + } + } + .switch()?; + Ok(data) +} diff --git a/crates/analytics/src/outgoing_webhook_event/events.rs b/crates/analytics/src/outgoing_webhook_event/events.rs new file mode 100644 index 000000000000..e742387e1eb5 --- /dev/null +++ b/crates/analytics/src/outgoing_webhook_event/events.rs @@ -0,0 +1,90 @@ +use api_models::analytics::{outgoing_webhook_event::OutgoingWebhookLogsRequest, Granularity}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait OutgoingWebhookLogsFilterAnalytics: LoadRow {} + +pub async fn get_outgoing_webhook_event( + merchant_id: &String, + query_param: OutgoingWebhookLogsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + OutgoingWebhookLogsFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::OutgoingWebhookEvent); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + query_builder + .add_filter_clause("payment_id", query_param.payment_id) + .switch()?; + + if let Some(event_id) = query_param.event_id { + query_builder + .add_filter_clause("event_id", &event_id) + .switch()?; + } + if let Some(refund_id) = query_param.refund_id { + query_builder + .add_filter_clause("refund_id", &refund_id) + .switch()?; + } + if let Some(dispute_id) = query_param.dispute_id { + query_builder + .add_filter_clause("dispute_id", &dispute_id) + .switch()?; + } + if let Some(mandate_id) = query_param.mandate_id { + query_builder + .add_filter_clause("mandate_id", &mandate_id) + .switch()?; + } + if let Some(payment_method_id) = query_param.payment_method_id { + query_builder + .add_filter_clause("payment_method_id", &payment_method_id) + .switch()?; + } + if let Some(attempt_id) = query_param.attempt_id { + query_builder + .add_filter_clause("attempt_id", &attempt_id) + .switch()?; + } + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct OutgoingWebhookLogsResult { + pub merchant_id: String, + pub event_id: String, + pub event_type: String, + pub outgoing_webhook_event_type: String, + pub payment_id: String, + pub refund_id: Option, + pub attempt_id: Option, + pub dispute_id: Option, + pub payment_method_id: Option, + pub mandate_id: Option, + pub content: Option, + pub is_error: bool, + pub error: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, +} diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index cdd2647e4e71..e32b85a53672 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -429,6 +429,8 @@ impl ToSql for AnalyticsCollection { Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) .attach_printable("ApiEvents table is not implemented for Sqlx"))?, Self::PaymentIntent => Ok("payment_intent".to_string()), + Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, } } } diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 8b1bdbd1ab92..8da4655e255b 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -26,6 +26,7 @@ pub enum AnalyticsCollection { SdkEvents, ApiEvents, PaymentIntent, + OutgoingWebhookEvent, } #[allow(dead_code)] diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 0263427b0fde..e0d3fa671b60 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -12,6 +12,7 @@ use self::{ pub use crate::payments::TimeRange; pub mod api_event; +pub mod outgoing_webhook_event; pub mod payments; pub mod refunds; pub mod sdk_events; diff --git a/crates/api_models/src/analytics/outgoing_webhook_event.rs b/crates/api_models/src/analytics/outgoing_webhook_event.rs new file mode 100644 index 000000000000..b6f0aca056fd --- /dev/null +++ b/crates/api_models/src/analytics/outgoing_webhook_event.rs @@ -0,0 +1,10 @@ +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct OutgoingWebhookLogsRequest { + pub payment_id: String, + pub event_id: Option, + pub refund_id: Option, + pub dispute_id: Option, + pub mandate_id: Option, + pub payment_method_id: Option, + pub attempt_id: Option, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 457d3fde05b7..6d9bd5db3429 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -17,7 +17,9 @@ use common_utils::{ use crate::{ admin::*, - analytics::{api_event::*, sdk_events::*, *}, + analytics::{ + api_event::*, outgoing_webhook_event::OutgoingWebhookLogsRequest, sdk_events::*, *, + }, api_keys::*, cards_info::*, disputes::*, @@ -89,7 +91,8 @@ impl_misc_api_event_type!( ApiLogsRequest, GetApiEventMetricRequest, SdkEventsRequest, - ReportRequest + ReportRequest, + OutgoingWebhookLogsRequest ); #[cfg(feature = "stripe")] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index f31e908e0dc3..c62de5bd29ab 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -4,7 +4,7 @@ pub mod routes { use actix_web::{web, Responder, Scope}; use analytics::{ api_event::api_events_core, errors::AnalyticsError, lambda_utils::invoke_lambda, - sdk_events::sdk_events_core, + outgoing_webhook_event::outgoing_webhook_events_core, sdk_events::sdk_events_core, }; use api_models::analytics::{ GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, @@ -71,6 +71,10 @@ pub mod routes { ) .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) + .service( + web::resource("outgoing_webhook_event_logs") + .route(web::get().to(get_outgoing_webhook_events)), + ) .service( web::resource("filters/api_events") .route(web::post().to(get_api_event_filters)), @@ -314,6 +318,30 @@ pub mod routes { .await } + pub async fn get_outgoing_webhook_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query< + api_models::analytics::outgoing_webhook_event::OutgoingWebhookLogsRequest, + >, + ) -> impl Responder { + let flow = AnalyticsFlow::GetOutgoingWebhookEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + outgoing_webhook_events_core(&state.pool, req, auth.merchant_account.merchant_id) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + pub async fn get_sdk_events( state: web::Data, req: actix_web::HttpRequest, diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index 3c7ba8b93df7..0127d07170fd 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -52,6 +52,7 @@ pub enum AnalyticsFlow { GenerateRefundReport, GetApiEventMetrics, GetApiEventFilters, + GetOutgoingWebhookEvents, } impl FlowMetric for AnalyticsFlow {} From e75b11e98ac4c8d37c842c8ee0ccf361dcb52793 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:16:16 +0530 Subject: [PATCH 322/443] feat(connector): [BOA/CYB] Store AVS response in connector_metadata (#3271) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/bankofamerica.rs | 6 +- .../connector/bankofamerica/transformers.rs | 194 ++++++++++++------ crates/router/src/connector/cybersource.rs | 6 +- .../src/connector/cybersource/transformers.rs | 146 ++++++++----- 4 files changed, 233 insertions(+), 119 deletions(-) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 1e0856a9ccc4..aeb3dafcfa21 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -205,7 +205,7 @@ impl ConnectorCommon for Bankofamerica { }; match response { transformers::BankOfAmericaErrorResponse::StandardError(response) => { - let (code, message) = match response.error_information { + let (code, connector_reason) = match response.error_information { Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), None => ( response @@ -218,13 +218,13 @@ impl ConnectorCommon for Bankofamerica { .map_or(error_message.to_string(), |message| message), ), }; - let connector_reason = match response.details { + let message = match response.details { Some(details) => details .iter() .map(|det| format!("{} : {}", det.field, det.reason)) .collect::>() .join(", "), - None => message.clone(), + None => connector_reason.clone(), }; Ok(ErrorResponse { diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index e024eb7a5019..6abe1b634df6 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -343,6 +343,30 @@ pub struct ClientReferenceInformation { code: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientProcessorInformation { + avs: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientRiskInformation { + rules: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ClientRiskInformationRules { + name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Avs { + code: String, + code_raw: String, +} + impl TryFrom<( &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, @@ -658,10 +682,12 @@ pub struct BankOfAmericaClientReferenceResponse { id: String, status: BankofamericaPaymentStatus, client_reference_information: ClientReferenceInformation, + processor_information: Option, + risk_information: Option, error_information: Option, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BankOfAmericaErrorInformationResponse { id: String, @@ -674,6 +700,55 @@ pub struct BankOfAmericaErrorInformation { message: Option, } +impl + From<( + &BankOfAmericaErrorInformationResponse, + types::ResponseRouterData, + Option, + )> for types::RouterData +{ + fn from( + (error_response, item, transaction_status): ( + &BankOfAmericaErrorInformationResponse, + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + T, + types::PaymentsResponseData, + >, + Option, + ), + ) -> Self { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + match transaction_status { + Some(status) => Self { + response, + status, + ..item.data + }, + None => Self { + response, + ..item.data + }, + } + } +} + fn get_error_response_if_failure( (info_response, status, http_code): ( &BankOfAmericaClientReferenceResponse, @@ -684,6 +759,7 @@ fn get_error_response_if_failure( if utils::is_payment_failure(status) { Some(types::ErrorResponse::from(( &info_response.error_information, + &info_response.risk_information, http_code, info_response.id.clone(), ))) @@ -706,7 +782,10 @@ fn get_payment_response( resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), redirection_data: None, mandate_reference: None, - connector_metadata: None, + connector_metadata: info_response + .processor_information + .as_ref() + .map(|processor_information| serde_json::json!({"avs_response": processor_information.avs})), network_txn_id: None, connector_response_reference_id: Some( info_response @@ -752,26 +831,13 @@ impl ..item.data }) } - BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id), - }) - }, - status: enums::AttemptStatus::Failure, - ..item.data - }), + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))) + } } } } @@ -806,24 +872,9 @@ impl ..item.data }) } - BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id), - }) - }, - ..item.data - }), + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } @@ -858,24 +909,9 @@ impl ..item.data }) } - BankOfAmericaPaymentsResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id), - }) - }, - ..item.data - }), + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from((&error_response.clone(), item, None))) + } } } } @@ -927,10 +963,12 @@ impl app_response.application_information.status, item.data.request.is_auto_capture()?, )); + let risk_info: Option = None; if utils::is_payment_failure(status) { Ok(Self { response: Err(types::ErrorResponse::from(( &app_response.error_information, + &risk_info, item.http_code, app_response.id.clone(), ))), @@ -1213,8 +1251,8 @@ pub struct BankOfAmericaAuthenticationErrorResponse { #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum BankOfAmericaErrorResponse { - StandardError(BankOfAmericaStandardErrorResponse), AuthenticationError(BankOfAmericaAuthenticationErrorResponse), + StandardError(BankOfAmericaStandardErrorResponse), } #[derive(Debug, Deserialize, Clone)] @@ -1235,29 +1273,53 @@ pub struct AuthenticationErrorInformation { pub rmsg: String, } -impl From<(&Option, u16, String)> for types::ErrorResponse { +impl + From<( + &Option, + &Option, + u16, + String, + )> for types::ErrorResponse +{ fn from( - (error_data, status_code, transaction_id): ( + (error_data, risk_information, status_code, transaction_id): ( &Option, + &Option, u16, String, ), ) -> Self { - let error_message = error_data + let avs_message = risk_information .clone() - .and_then(|error_details| error_details.message); + .map(|client_risk_information| { + client_risk_information.rules.map(|rules| { + rules + .iter() + .map(|risk_info| format!(" , {}", risk_info.name)) + .collect::>() + .join("") + }) + }) + .unwrap_or(Some("".to_string())); let error_reason = error_data + .clone() + .map(|error_details| { + error_details.message.unwrap_or("".to_string()) + + &avs_message.unwrap_or("".to_string()) + }) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_data .clone() .and_then(|error_details| error_details.reason); Self { - code: error_reason + code: error_message .clone() .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason + message: error_message .clone() .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_message.clone(), + reason: Some(error_reason.clone()), status_code, attempt_status: Some(enums::AttemptStatus::Failure), connector_transaction_id: Some(transaction_id.clone()), diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 33503102e4b5..6c4ea4c61fe0 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -124,7 +124,7 @@ impl ConnectorCommon for Cybersource { }; match response { transformers::CybersourceErrorResponse::StandardError(response) => { - let (code, message) = match response.error_information { + let (code, connector_reason) = match response.error_information { Some(ref error_info) => (error_info.reason.clone(), error_info.message.clone()), None => ( response @@ -137,13 +137,13 @@ impl ConnectorCommon for Cybersource { .map_or(error_message.to_string(), |message| message), ), }; - let connector_reason = match response.details { + let message = match response.details { Some(details) => details .iter() .map(|det| format!("{} : {}", det.field, det.reason)) .collect::>() .join(", "), - None => message.clone(), + None => connector_reason.clone(), }; Ok(types::ErrorResponse { diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index bc69fb78129f..8ae2ce29e5bc 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1105,6 +1105,8 @@ pub struct CybersourceClientReferenceResponse { id: String, status: CybersourcePaymentStatus, client_reference_information: ClientReferenceInformation, + processor_information: Option, + risk_information: Option, token_information: Option, error_information: Option, } @@ -1136,6 +1138,30 @@ pub struct ClientReferenceInformation { code: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientProcessorInformation { + avs: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Avs { + code: String, + code_raw: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientRiskInformation { + rules: Option>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ClientRiskInformationRules { + name: String, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceTokenInformation { @@ -1152,10 +1178,11 @@ impl From<( &CybersourceErrorInformationResponse, types::ResponseRouterData, + Option, )> for types::RouterData { fn from( - (error_response, item): ( + (error_response, item, transaction_status): ( &CybersourceErrorInformationResponse, types::ResponseRouterData< F, @@ -1163,25 +1190,35 @@ impl T, types::PaymentsResponseData, >, + Option, ), ) -> Self { - Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message.clone(), - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id.clone()), - }) + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + match transaction_status { + Some(status) => Self { + response, + status, + ..item.data + }, + None => Self { + response, + ..item.data }, - ..item.data } } } @@ -1196,6 +1233,7 @@ fn get_error_response_if_failure( if utils::is_payment_failure(status) { Some(types::ErrorResponse::from(( &info_response.error_information, + &info_response.risk_information, http_code, info_response.id.clone(), ))) @@ -1229,7 +1267,10 @@ fn get_payment_response( resource_id: types::ResponseId::ConnectorTransactionId(info_response.id.clone()), redirection_data: None, mandate_reference, - connector_metadata: None, + connector_metadata: info_response + .processor_information + .as_ref() + .map(|processor_information| serde_json::json!({"avs_response": processor_information.avs})), network_txn_id: None, connector_response_reference_id: Some( info_response @@ -1276,25 +1317,11 @@ impl ..item.data }) } - CybersourcePaymentsResponse::ErrorInformation(error_response) => { - let error_reason = &error_response.error_information.reason; - Ok(Self { - response: Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id.clone()), - }), - status: enums::AttemptStatus::Failure, - ..item.data - }) - } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))), } } } @@ -1330,7 +1357,7 @@ impl }) } CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { - Ok(Self::from((&error_response.clone(), item))) + Ok(Self::from((&error_response.clone(), item, None))) } } } @@ -1367,7 +1394,7 @@ impl }) } CybersourcePaymentsResponse::ErrorInformation(ref error_response) => { - Ok(Self::from((&error_response.clone(), item))) + Ok(Self::from((&error_response.clone(), item, None))) } } } @@ -1556,10 +1583,12 @@ impl )); let incremental_authorization_allowed = Some(status == enums::AttemptStatus::Authorized); + let risk_info: Option = None; if utils::is_payment_failure(status) { Ok(Self { response: Err(types::ErrorResponse::from(( &app_response.error_information, + &risk_info, item.http_code, app_response.id.clone(), ))), @@ -1782,30 +1811,53 @@ pub struct AuthenticationErrorInformation { pub rmsg: String, } -impl From<(&Option, u16, String)> for types::ErrorResponse { +impl + From<( + &Option, + &Option, + u16, + String, + )> for types::ErrorResponse +{ fn from( - (error_data, status_code, transaction_id): ( + (error_data, risk_information, status_code, transaction_id): ( &Option, + &Option, u16, String, ), ) -> Self { - let error_message = error_data + let avs_message = risk_information .clone() - .and_then(|error_details| error_details.message); - + .map(|client_risk_information| { + client_risk_information.rules.map(|rules| { + rules + .iter() + .map(|risk_info| format!(" , {}", risk_info.name)) + .collect::>() + .join("") + }) + }) + .unwrap_or(Some("".to_string())); let error_reason = error_data + .clone() + .map(|error_details| { + error_details.message.unwrap_or("".to_string()) + + &avs_message.unwrap_or("".to_string()) + }) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_data .clone() .and_then(|error_details| error_details.reason); Self { - code: error_reason + code: error_message .clone() .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason + message: error_message .clone() .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_message.clone(), + reason: Some(error_reason.clone()), status_code, attempt_status: Some(enums::AttemptStatus::Failure), connector_transaction_id: Some(transaction_id.clone()), From af43b07e4394458db478bc16e5fb8d3b0d636a31 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:16:51 +0530 Subject: [PATCH 323/443] fix(refund): add merchant_connector_id in refund (#3303) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/refunds.rs | 3 +++ crates/router/src/core/refunds.rs | 2 ++ openapi/openapi_spec.json | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index e89de9c58934..1a0668023f02 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -127,7 +127,10 @@ pub struct RefundResponse { /// The connector used for the refund and the corresponding payment #[schema(example = "stripe")] pub connector: String, + /// The id of business profile for this refund pub profile_id: Option, + /// The merchant_connector_id of the processor through which this payment went through + pub merchant_connector_id: Option, } #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 6cc118b0f3c7..e60c341dedcf 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -650,6 +650,7 @@ pub async fn validate_and_create_refund( .set_attempt_id(payment_attempt.attempt_id.clone()) .set_refund_reason(req.reason) .set_profile_id(payment_intent.profile_id.clone()) + .set_merchant_connector_id(payment_attempt.merchant_connector_id.clone()) .to_owned(); let refund = match db @@ -776,6 +777,7 @@ impl ForeignFrom for api::RefundResponse { created_at: Some(refund.created_at), updated_at: Some(refund.updated_at), connector: refund.connector, + merchant_connector_id: refund.merchant_connector_id, } } } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 7a2b5504e0ec..3e582cfed528 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11679,6 +11679,12 @@ }, "profile_id": { "type": "string", + "description": "The id of business profile for this refund", + "nullable": true + }, + "merchant_connector_id": { + "type": "string", + "description": "The merchant_connector_id of the processor through which this payment went through", "nullable": true } } From 9f6ef3f2240052053b5b7df0a13a5503d8141d56 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:59:24 +0530 Subject: [PATCH 324/443] chore: remove connector auth TOML files from `.gitignore` and `.dockerignore` (#3330) --- .dockerignore | 4 ---- .gitignore | 4 ---- crates/router/tests/connectors/sample_auth.toml | 2 +- crates/test_utils/tests/sample_auth.toml | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.dockerignore b/.dockerignore index 62804a712fa1..81ef10ad2133 100644 --- a/.dockerignore +++ b/.dockerignore @@ -261,7 +261,3 @@ result* # node_modules node_modules/ - -**/connector_auth.toml -**/sample_auth.toml -**/auth.toml diff --git a/.gitignore b/.gitignore index 62804a712fa1..81ef10ad2133 100644 --- a/.gitignore +++ b/.gitignore @@ -261,7 +261,3 @@ result* # node_modules node_modules/ - -**/connector_auth.toml -**/sample_auth.toml -**/auth.toml diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index ff179f745065..68cf6f680355 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -108,7 +108,7 @@ api_key = "API Key" [iatapay] key1 = "key1" api_key = "api_key" -api_secret = "secrect" +api_secret = "secret" [dummyconnector] api_key = "API Key" diff --git a/crates/test_utils/tests/sample_auth.toml b/crates/test_utils/tests/sample_auth.toml index 0ae7c40d42d3..08b24817c24e 100644 --- a/crates/test_utils/tests/sample_auth.toml +++ b/crates/test_utils/tests/sample_auth.toml @@ -108,7 +108,7 @@ api_key = "API Key" [iatapay] key1 = "key1" api_key = "api_key" -api_secret = "secrect" +api_secret = "secret" [dummyconnector] api_key = "API Key" From 6fb3b00e82d1e3c03dc1c816ffa6353cc7991a53 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:50:45 +0530 Subject: [PATCH 325/443] feat(connector): [cybersource] Implement 3DS flow for cards (#3290) Co-authored-by: DEEPANSHU BANSAL Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/router/src/connector/cybersource.rs | 239 +++++- .../src/connector/cybersource/transformers.rs | 805 +++++++++++++++++- crates/router/src/connector/utils.rs | 12 + crates/router/src/core/payments.rs | 20 + crates/router/src/core/payments/flows.rs | 2 - .../src/core/payments/flows/authorize_flow.rs | 4 +- .../payments/flows/complete_authorize_flow.rs | 11 +- .../router/src/core/payments/transformers.rs | 12 +- crates/router/src/services/api.rs | 108 +++ crates/router/src/types.rs | 3 + 13 files changed, 1175 insertions(+), 44 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 94f71fa3f704..e20f9c1b65d2 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -351,6 +351,7 @@ stripe = { payment_method = "bank_transfer" } nuvei = { payment_method = "card" } shift4 = { payment_method = "card" } bluesnap = { payment_method = "card" } +cybersource = {payment_method = "card"} nmi = {payment_method = "card"} [dummy_connector] diff --git a/config/development.toml b/config/development.toml index 272b36417137..5732d5f0d1de 100644 --- a/config/development.toml +++ b/config/development.toml @@ -428,6 +428,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +cybersource = {payment_method = "card"} nmi = {payment_method = "card"} [connector_customer] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index e55353f89033..c6934a64671f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -241,6 +241,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +cybersource = {payment_method = "card"} nmi = {payment_method = "card"} [dummy_connector] diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 6c4ea4c61fe0..69159c10c8af 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -12,6 +12,7 @@ use time::OffsetDateTime; use transformers as cybersource; use url::Url; +use super::utils::{PaymentsAuthorizeRequestData, RouterData}; use crate::{ configs::settings, connector::{utils as connector_utils, utils::RefundsRequestData}, @@ -286,6 +287,8 @@ impl api::PaymentIncrementalAuthorization for Cybersource {} impl api::MandateSetup for Cybersource {} impl api::ConnectorAccessToken for Cybersource {} impl api::PaymentToken for Cybersource {} +impl api::PaymentsPreProcessing for Cybersource {} +impl api::PaymentsCompleteAuthorize for Cybersource {} impl api::ConnectorMandateRevoke for Cybersource {} impl @@ -472,6 +475,113 @@ impl ConnectorIntegration for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + 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::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let redirect_response = req.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => Ok(format!( + "{}risk/v1/authentications", + self.base_url(connectors) + )), + Some(_) | None => Ok(format!( + "{}risk/v1/authentication-results", + self.base_url(connectors) + )), + } + } + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + req, + ))?; + let connector_req = + cybersource::CybersourcePreProcessingRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourcePreProcessingResponse = res + .response + .parse_struct("Cybersource AuthEnrollmentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Cybersource { @@ -672,13 +782,20 @@ impl ConnectorIntegration CustomResult { - Ok(format!( - "{}pts/v2/payments/", - api::ConnectorCommon::base_url(self, connectors) - )) + if req.is_three_ds() && req.request.is_card() { + Ok(format!( + "{}risk/v1/authentication-setups", + api::ConnectorCommon::base_url(self, connectors) + )) + } else { + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) + )) + } } fn get_request_body( @@ -692,9 +809,15 @@ impl ConnectorIntegration CustomResult { + if data.is_three_ds() && data.request.is_card() { + let response: cybersource::CybersourceAuthSetupResponse = res + .response + .parse_struct("Cybersource AuthSetupResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } else { + let response: cybersource::CybersourcePaymentsResponse = res + .response + .parse_struct("Cybersource PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + } + + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}pts/v2/payments/", + api::ConnectorCommon::base_url(self, connectors) + )) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = + cybersource::CybersourcePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: types::Response, + ) -> CustomResult { let response: cybersource::CybersourcePaymentsResponse = res .response .parse_struct("Cybersource PaymentResponse") diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 8ae2ce29e5bc..e83b23603e9b 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -1,6 +1,7 @@ use api_models::payments; use base64::Engine; -use common_utils::pii; +use common_utils::{ext_traits::ValueExt, pii}; +use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -8,10 +9,12 @@ use serde_json::Value; use crate::{ connector::utils::{ self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, + PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData, PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RouterData, }, consts, core::errors, + services, types::{ self, api::{self, enums as api_enums}, @@ -200,7 +203,7 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { action_list, action_token_types, authorization_options, - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), payment_solution: solution.map(String::from), }; Ok(Self { @@ -220,6 +223,8 @@ pub struct CybersourcePaymentsRequest { order_information: OrderInformationWithBill, client_reference_information: ClientReferenceInformation, #[serde(skip_serializing_if = "Option::is_none")] + consumer_authentication_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] merchant_defined_information: Option>, } @@ -229,12 +234,22 @@ pub struct ProcessingInformation { action_list: Option>, action_token_types: Option>, authorization_options: Option, - commerce_indicator: CybersourceCommerceIndicator, + commerce_indicator: String, capture: Option, capture_options: Option, payment_solution: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformation { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + directory_server_transaction_id: Option, + specification_version: Option, +} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MerchantDefinedInformation { @@ -282,12 +297,6 @@ pub enum CybersourcePaymentInitiatorTypes { Customer, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum CybersourceCommerceIndicator { - Internet, -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { @@ -450,6 +459,16 @@ impl From<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> } } +impl From<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + impl From<( &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, @@ -489,7 +508,56 @@ impl action_token_types, authorization_options, capture_options: None, - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), + } + } +} + +impl + From<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &CybersourceConsumerAuthValidateResponse, + )> for ProcessingInformation +{ + fn from( + (item, solution, three_ds_data): ( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &CybersourceConsumerAuthValidateResponse, + ), + ) -> Self { + let (action_list, action_token_types, authorization_options) = + if item.router_data.request.setup_future_usage.is_some() { + ( + Some(vec![CybersourceActionsList::TokenCreate]), + Some(vec![CybersourceActionsTokenType::PaymentInstrument]), + Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, + }, + merchant_intitiated_transaction: None, + }), + ) + } else { + (None, None, None) + }; + Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + action_list, + action_token_types, + authorization_options, + capture_options: None, + commerce_indicator: three_ds_data + .indicator + .to_owned() + .unwrap_or(String::from("internet")), } } } @@ -516,6 +584,28 @@ impl } } +impl + From<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to: Some(bill_to), + } + } +} + // for cybersource each item in Billing is mandatory fn build_bill_to( address_details: &payments::Address, @@ -602,6 +692,84 @@ impl payment_information, order_information, client_reference_information, + consumer_authentication_information: None, + merchant_defined_information, + }) + } +} + +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + let client_reference_information = ClientReferenceInformation::from(item); + + let three_ds_info: CybersourceThreeDSMetadata = item + .router_data + .request + .connector_meta + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "connector_meta", + })? + .parse_value("CybersourceThreeDSMetadata") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; + + let processing_information = + ProcessingInformation::from((item, None, &three_ds_info.three_ds_data)); + + let consumer_authentication_information = Some(CybersourceConsumerAuthInformation { + ucaf_collection_indicator: three_ds_info.three_ds_data.ucaf_collection_indicator, + cavv: three_ds_info.three_ds_data.cavv, + ucaf_authentication_data: three_ds_info.three_ds_data.ucaf_authentication_data, + xid: three_ds_info.three_ds_data.xid, + directory_server_transaction_id: three_ds_info + .three_ds_data + .directory_server_transaction_id, + specification_version: three_ds_info.three_ds_data.specification_version, + }); + + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information, merchant_defined_information, }) } @@ -647,6 +815,7 @@ impl payment_information, order_information, client_reference_information, + consumer_authentication_information: None, merchant_defined_information, }) } @@ -689,6 +858,7 @@ impl payment_information, order_information, client_reference_information, + consumer_authentication_information: None, merchant_defined_information, }) } @@ -747,6 +917,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } } @@ -810,6 +981,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } payments::PaymentMethodData::CardRedirect(_) @@ -832,6 +1004,64 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> } } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthSetupRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> + for CybersourceAuthSetupRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + let client_reference_information = ClientReferenceInformation::from(item); + Ok(Self { + payment_information, + client_reference_information, + }) + } + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ) + .into()) + } + } + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsCaptureRequest { @@ -870,7 +1100,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> action_token_types: None, authorization_options: None, capture: None, - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), payment_solution: None, }, order_information: OrderInformationWithBill { @@ -909,7 +1139,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout reason: "5".to_owned(), }), }), - commerce_indicator: CybersourceCommerceIndicator::Internet, + commerce_indicator: String::from("internet"), capture: None, capture_options: None, payment_solution: None, @@ -1118,6 +1348,29 @@ pub struct CybersourceErrorInformationResponse { error_information: CybersourceErrorInformation, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationResponse { + access_token: String, + device_data_collection_url: String, + reference_id: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthSetupInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourceAuthSetupResponse { + ClientAuthSetupInfo(ClientAuthSetupInfoResponse), + ErrorInformation(CybersourceErrorInformationResponse), +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentsIncrementalAuthorizationResponse { @@ -1326,6 +1579,492 @@ impl } } +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourceAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourceAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourceAuthSetupResponse::ClientAuthSetupInfo(info_response) => Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::CybersourceAuthSetup { + access_token: info_response + .consumer_authentication_information + .access_token, + ddc_url: info_response + .consumer_authentication_information + .device_data_collection_url, + reference_id: info_response + .consumer_authentication_information + .reference_id, + }), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed: None, + }), + ..item.data + }), + CybersourceAuthSetupResponse::ErrorInformation(error_response) => { + let error_reason = error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason; + Ok(Self { + response: Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }), + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationRequest { + return_url: String, + reference_id: String, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthEnrollmentRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationRequest, + order_information: OrderInformationWithBill, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct CybersourceRedirectionAuthResponse { + pub transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationValidateRequest { + authentication_transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceAuthValidateRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationValidateRequest, + order_information: OrderInformation, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum CybersourcePreProcessingRequest { + AuthEnrollment(CybersourceAuthEnrollmentRequest), + AuthValidate(CybersourceAuthValidateRequest), +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsPreProcessingRouterData>> + for CybersourcePreProcessingRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsPreProcessingRouterData>, + ) -> Result { + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "payment_method_data", + }, + )?; + let payment_information = match payment_method_data { + payments::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + Ok(PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + })) + } + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + )) + } + }?; + + let redirect_response = item.router_data.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + + let amount_details = Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "currency", + }, + )?, + }; + + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => { + let reference_id = param + .clone() + .peek() + .split_once('=') + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.params.reference_id", + })? + .1 + .to_string(); + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill { + amount_details, + bill_to: Some(bill_to), + }; + Ok(Self::AuthEnrollment(CybersourceAuthEnrollmentRequest { + payment_information, + client_reference_information, + consumer_authentication_information: + CybersourceConsumerAuthInformationRequest { + return_url: item.router_data.request.get_complete_authorize_url()?, + reference_id, + }, + order_information, + })) + } + Some(_) | None => { + let redirect_payload: CybersourceRedirectionAuthResponse = redirect_response + .payload + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.payload", + })? + .peek() + .clone() + .parse_value("CybersourceRedirectionAuthResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let order_information = OrderInformation { amount_details }; + Ok(Self::AuthValidate(CybersourceAuthValidateRequest { + payment_information, + client_reference_information, + consumer_authentication_information: + CybersourceConsumerAuthInformationValidateRequest { + authentication_transaction_id: redirect_payload.transaction_id, + }, + order_information, + })) + } + } + } +} + +impl TryFrom<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "payment_method_data", + }, + )?; + match payment_method_data { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ) + .into()) + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceAuthEnrollmentStatus { + PendingAuthentication, + AuthenticationSuccessful, + AuthenticationFailed, +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthValidateResponse { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + specification_version: Option, + directory_server_transaction_id: Option, + indicator: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CybersourceThreeDSMetadata { + three_ds_data: CybersourceConsumerAuthValidateResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourceConsumerAuthInformationEnrollmentResponse { + access_token: Option, + step_up_url: Option, + //Added to segregate the three_ds_data in a separate struct + #[serde(flatten)] + validate_response: CybersourceConsumerAuthValidateResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthCheckInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: CybersourceConsumerAuthInformationEnrollmentResponse, + status: CybersourceAuthEnrollmentStatus, + error_information: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum CybersourcePreProcessingResponse { + ClientAuthCheckInfo(Box), + ErrorInformation(CybersourceErrorInformationResponse), +} + +impl From for enums::AttemptStatus { + fn from(item: CybersourceAuthEnrollmentStatus) -> Self { + match item { + CybersourceAuthEnrollmentStatus::PendingAuthentication => Self::AuthenticationPending, + CybersourceAuthEnrollmentStatus::AuthenticationSuccessful => { + Self::AuthenticationSuccessful + } + CybersourceAuthEnrollmentStatus::AuthenticationFailed => Self::AuthenticationFailed, + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePreProcessingResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePreProcessingResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePreProcessingResponse::ClientAuthCheckInfo(info_response) => { + let status = enums::AttemptStatus::from(info_response.status); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + let response = Err(types::ErrorResponse::from(( + &info_response.error_information, + &risk_info, + item.http_code, + info_response.id.clone(), + ))); + + Ok(Self { + status, + response, + ..item.data + }) + } else { + let connector_response_reference_id = Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ); + + let redirection_data = match ( + info_response + .consumer_authentication_information + .access_token, + info_response + .consumer_authentication_information + .step_up_url, + ) { + (Some(access_token), Some(step_up_url)) => { + Some(services::RedirectForm::CybersourceConsumerAuth { + access_token, + step_up_url, + }) + } + _ => None, + }; + let three_ds_data = serde_json::to_value( + info_response + .consumer_authentication_information + .validate_response, + ) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data, + mandate_reference: None, + connector_metadata: Some( + serde_json::json!({"three_ds_data":three_ds_data}), + ), + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + } + CybersourcePreProcessingResponse::ErrorInformation(ref error_response) => { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + Ok(Self { + response, + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + CybersourcePaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + CybersourcePaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + CybersourcePaymentsResponse::ErrorInformation(ref error_response) => Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))), + } + } +} + impl TryFrom< types::ResponseRouterData< @@ -1463,25 +2202,29 @@ impl ..item.data }) } - CybersourceSetupMandatesResponse::ErrorInformation(error_response) => Ok(Self { - response: { - let error_reason = &error_response.error_information.reason; - Err(types::ErrorResponse { - code: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_reason - .clone() - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: error_response.error_information.message, - status_code: item.http_code, - attempt_status: None, - connector_transaction_id: Some(error_response.id.clone()), - }) - }, - status: enums::AttemptStatus::Failure, - ..item.data - }), + CybersourceSetupMandatesResponse::ErrorInformation(ref error_response) => { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + Ok(Self { + response, + status: enums::AttemptStatus::Failure, + ..item.data + }) + } } } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 55173f9b339e..1040f020839d 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -273,6 +273,7 @@ pub trait PaymentsPreProcessingData { fn get_webhook_url(&self) -> Result; fn get_return_url(&self) -> Result; fn get_browser_info(&self) -> Result; + fn get_complete_authorize_url(&self) -> Result; } impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { @@ -317,6 +318,11 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { .clone() .ok_or_else(missing_field_err("browser_info")) } + fn get_complete_authorize_url(&self) -> Result { + self.complete_authorize_url + .clone() + .ok_or_else(missing_field_err("complete_authorize_url")) + } } pub trait PaymentsCaptureRequestData { @@ -592,6 +598,7 @@ pub trait PaymentsCompleteAuthorizeRequestData { fn is_auto_capture(&self) -> Result; fn get_email(&self) -> Result; fn get_redirect_response_payload(&self) -> Result; + fn get_complete_authorize_url(&self) -> Result; } impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData { @@ -616,6 +623,11 @@ impl PaymentsCompleteAuthorizeRequestData for types::CompleteAuthorizeData { .into(), ) } + fn get_complete_authorize_url(&self) -> Result { + self.complete_authorize_url + .clone() + .ok_or_else(missing_field_err("complete_authorize_url")) + } } pub trait PaymentsSyncRequestData { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index ff4934e1efcb..21cdec92ccb4 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1489,6 +1489,22 @@ where router_data = router_data.preprocessing_steps(state, connector).await?; (router_data, false) + } else if connector.connector_name == router_types::Connector::Cybersource + && is_operation_complete_authorize(&operation) + && router_data.auth_type == storage_enums::AuthenticationType::ThreeDs + { + router_data = router_data.preprocessing_steps(state, connector).await?; + + // Should continue the flow only if no redirection_data is returned else a response with redirection form shall be returned + let should_continue = matches!( + router_data.response, + Ok(router_types::PaymentsResponseData::TransactionResponse { + redirection_data: None, + .. + }) + ) && router_data.status + != common_enums::AttemptStatus::AuthenticationFailed; + (router_data, should_continue) } else { (router_data, should_continue_payment) } @@ -2106,6 +2122,10 @@ pub fn is_operation_confirm(operation: &Op) -> bool { matches!(format!("{operation:?}").as_str(), "PaymentConfirm") } +pub fn is_operation_complete_authorize(operation: &Op) -> bool { + matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") +} + #[cfg(feature = "olap")] pub async fn list_payments( state: AppState, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 27ddd3f6d81c..6dd692f15259 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -154,7 +154,6 @@ default_imp_for_complete_authorize!( connector::Checkout, connector::Coinbase, connector::Cryptopay, - connector::Cybersource, connector::Dlocal, connector::Fiserv, connector::Forte, @@ -873,7 +872,6 @@ default_imp_for_pre_processing_steps!( connector::Checkout, connector::Coinbase, connector::Cryptopay, - connector::Cybersource, connector::Dlocal, connector::Iatapay, connector::Fiserv, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index c934c7c2cd67..07af15a336d9 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -412,6 +412,7 @@ impl TryFrom for types::PaymentsPreProcessingData browser_info: data.browser_info, surcharge_details: data.surcharge_details, connector_transaction_id: None, + redirect_response: None, }) } } @@ -431,10 +432,11 @@ impl TryFrom for types::PaymentsPreProcessingData order_details: None, router_return_url: None, webhook_url: None, - complete_authorize_url: None, + complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: None, connector_transaction_id: data.connector_transaction_id, + redirect_response: data.redirect_response, }) } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 2d52a145feae..68d0ee8d475f 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -203,10 +203,19 @@ pub async fn complete_authorize_preprocessing_steps( ], ); + let mut router_data_request = router_data.request.to_owned(); + + if let Ok(types::PaymentsResponseData::TransactionResponse { + connector_metadata, .. + }) = &resp.response + { + router_data_request.connector_meta = connector_metadata.to_owned(); + }; + let authorize_router_data = payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( resp.clone(), - router_data.request.to_owned(), + router_data_request, resp.response, ); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5a3a322fb14d..dffcff23595b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1425,6 +1425,9 @@ impl TryFrom> for types::CompleteAuthoriz fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; + let router_base_url = &additional_data.router_base_url; + let connector_name = &additional_data.connector_name; + let attempt = &payment_data.payment_attempt; let browser_info: Option = payment_data .payment_attempt .browser_info @@ -1446,7 +1449,11 @@ impl TryFrom> for types::CompleteAuthoriz .as_ref() .map(|surcharge_details| surcharge_details.final_amount) .unwrap_or(payment_data.amount.into()); - + let complete_authorize_url = Some(helpers::create_complete_authorize_url( + router_base_url, + attempt, + connector_name, + )); Ok(Self { setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), @@ -1463,6 +1470,8 @@ impl TryFrom> for types::CompleteAuthoriz connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, redirect_response, connector_meta: payment_data.payment_attempt.connector_metadata, + complete_authorize_url, + metadata: payment_data.payment_intent.metadata, }) } } @@ -1541,6 +1550,7 @@ impl TryFrom> for types::PaymentsPreProce browser_info, surcharge_details: payment_data.surcharge_details, connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, + redirect_response: None, }) } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index fdaaa87bf407..9eb06d675a07 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -789,6 +789,15 @@ pub enum RedirectForm { BlueSnap { payment_fields_token: String, // payment-field-token }, + CybersourceAuthSetup { + access_token: String, + ddc_url: String, + reference_id: String, + }, + CybersourceConsumerAuth { + access_token: String, + step_up_url: String, + }, Payme, Braintree { client_token: String, @@ -1426,6 +1435,105 @@ pub fn build_redirection_form( "))) }} } + RedirectForm::CybersourceAuthSetup { + access_token, + ddc_url, + reference_id, + } => { + maud::html! { + (maud::DOCTYPE) + html { + head { + meta name="viewport" content="width=device-width, initial-scale=1"; + } + body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { + + div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } + + (PreEscaped(r#""#)) + + (PreEscaped(r#" + + "#)) + + + h3 style="text-align: center;" { "Please wait while we process your payment..." } + } + + (PreEscaped(r#""#)) + (PreEscaped(format!("
+ +
"))) + (PreEscaped(r#""#)) + (PreEscaped(format!(" + "))) + }} + } + RedirectForm::CybersourceConsumerAuth { + access_token, + step_up_url, + } => { + maud::html! { + (maud::DOCTYPE) + html { + head { + meta name="viewport" content="width=device-width, initial-scale=1"; + } + body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { + + div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } + + (PreEscaped(r#""#)) + + (PreEscaped(r#" + + "#)) + + + h3 style="text-align: center;" { "Please wait while we process your payment..." } + } + + // This is the iframe recommended by cybersource but the redirection happens inside this iframe once otp + // is received and we lose control of the redirection on user client browser, so to avoid that we have removed this iframe and directly consumed it. + // (PreEscaped(r#""#)) + (PreEscaped(format!("
+ +
"))) + (PreEscaped(r#""#)) + }} + } RedirectForm::Payme => { maud::html! { (maud::DOCTYPE) diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7cd45a0192f0..3521a82a5a87 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -490,6 +490,7 @@ pub struct PaymentsPreProcessingData { pub surcharge_details: Option, pub browser_info: Option, pub connector_transaction_id: Option, + pub redirect_response: Option, } #[derive(Debug, Clone)] @@ -510,6 +511,8 @@ pub struct CompleteAuthorizeData { pub browser_info: Option, pub connector_transaction_id: Option, pub connector_meta: Option, + pub complete_authorize_url: Option, + pub metadata: Option, } #[derive(Debug, Clone)] From cc3eefd317117d761cdcc76804f3510952d4cec2 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:38:25 +0530 Subject: [PATCH 326/443] feat: add support for card extended bin in payment attempt (#3312) --- crates/api_models/src/payments.rs | 9 ++++++--- crates/cards/src/validate.rs | 3 +++ crates/router/src/core/payments/helpers.rs | 4 ++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f9077500dd4f..cac94a07326a 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1129,6 +1129,7 @@ pub struct AdditionalCardInfo { pub bank_code: Option, pub last4: Option, pub card_isin: Option, + pub card_extended_bin: Option, pub card_exp_month: Option>, pub card_exp_year: Option>, pub card_holder_name: Option>, @@ -1665,6 +1666,7 @@ pub struct CardResponse { pub card_issuer: Option, pub card_issuing_country: Option, pub card_isin: Option, + pub card_extended_bin: Option, pub card_exp_month: Option>, pub card_exp_year: Option>, pub card_holder_name: Option>, @@ -1707,7 +1709,7 @@ pub enum VoucherData { #[serde(rename_all = "snake_case")] pub enum PaymentMethodDataResponse { #[serde(rename = "card")] - Card(CardResponse), + Card(Box), BankTransfer, Wallet, PayLater, @@ -2037,7 +2039,7 @@ pub struct PaymentsResponse { #[schema(example = 100)] pub amount: i64, - /// The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount, + /// The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount, /// If no surcharge_details, net_amount = amount #[schema(example = 110)] pub net_amount: i64, @@ -2531,6 +2533,7 @@ impl From for CardResponse { card_issuer: card.card_issuer, card_issuing_country: card.card_issuing_country, card_isin: card.card_isin, + card_extended_bin: card.card_extended_bin, card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_holder_name: card.card_holder_name, @@ -2541,7 +2544,7 @@ impl From for CardResponse { impl From for PaymentMethodDataResponse { fn from(payment_method_data: AdditionalPaymentData) -> Self { match payment_method_data { - AdditionalPaymentData::Card(card) => Self::Card(CardResponse::from(*card)), + AdditionalPaymentData::Card(card) => Self::Card(Box::new(CardResponse::from(*card))), AdditionalPaymentData::PayLater {} => Self::PayLater, AdditionalPaymentData::Wallet {} => Self::Wallet, AdditionalPaymentData::BankRedirect { .. } => Self::BankRedirect, diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index 87b04baa1a2c..0bb07b83dc68 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -42,6 +42,9 @@ impl CardNumber { .rev() .collect::() } + pub fn get_card_extended_bin(self) -> String { + self.0.peek().chars().take(8).collect::() + } } impl FromStr for CardNumber { diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 003c09b73817..7230d74e9a98 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3290,6 +3290,7 @@ pub async fn get_additional_payment_data( match pm_data { api_models::payments::PaymentMethodData::Card(card_data) => { let card_isin = Some(card_data.card_number.clone().get_card_isin()); + let card_extended_bin = Some(card_data.card_number.clone().get_card_extended_bin()); let last4 = Some(card_data.card_number.clone().get_last4()); if card_data.card_issuer.is_some() && card_data.card_network.is_some() @@ -3309,6 +3310,7 @@ pub async fn get_additional_payment_data( card_holder_name: card_data.card_holder_name.clone(), last4: last4.clone(), card_isin: card_isin.clone(), + card_extended_bin: card_extended_bin.clone(), }, )) } else { @@ -3332,6 +3334,7 @@ pub async fn get_additional_payment_data( card_issuing_country: card_info.card_issuing_country, last4: last4.clone(), card_isin: card_isin.clone(), + card_extended_bin: card_extended_bin.clone(), card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), card_holder_name: card_data.card_holder_name.clone(), @@ -3347,6 +3350,7 @@ pub async fn get_additional_payment_data( card_issuing_country: None, last4, card_isin, + card_extended_bin, card_exp_month: Some(card_data.card_exp_month.clone()), card_exp_year: Some(card_data.card_exp_year.clone()), card_holder_name: card_data.card_holder_name.clone(), From 469ea20214aa7c1a3b4b86520724c2509ae37b0b Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:00:58 +0530 Subject: [PATCH 327/443] fix: update amount_capturable based on intent_status and payment flow (#3278) --- crates/router/src/connector/utils.rs | 2 +- .../payments/operations/payment_response.rs | 68 ++++--- crates/router/src/types.rs | 168 +++++++++++++++++- 3 files changed, 192 insertions(+), 46 deletions(-) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 1040f020839d..8f028e37a9e5 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -117,7 +117,7 @@ where } enums::AttemptStatus::Charged => { let captured_amount = - types::Capturable::get_capture_amount(&self.request, payment_data); + types::Capturable::get_captured_amount(&self.request, payment_data); let total_capturable_amount = payment_data.payment_attempt.get_total_amount(); if Some(total_capturable_amount) == captured_amount { enums::AttemptStatus::Charged diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index adecf1b78ebe..9ab0b4f817f5 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -24,7 +24,7 @@ use crate::{ services::RedirectForm, types::{ self, api, - storage::{self, enums, payment_attempt::AttemptStatusExt}, + storage::{self, enums}, transformers::{ForeignFrom, ForeignTryFrom}, CaptureSyncResponse, }, @@ -499,15 +499,9 @@ async fn payment_response_update_tracker( error_message: Some(Some(err.message)), error_code: Some(Some(err.code)), error_reason: Some(err.reason), - amount_capturable: if status.is_terminal_status() - || router_data - .status - .maps_to_intent_status(enums::IntentStatus::Processing) - { - Some(0) - } else { - None - }, + amount_capturable: router_data + .request + .get_amount_capturable(&payment_data, status), updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), @@ -598,27 +592,33 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.merchant_id.clone(), ); - let (capture_updates, payment_attempt_update) = - match payment_data.multiple_capture_data { - Some(multiple_capture_data) => { - let capture_update = storage::CaptureUpdate::ResponseUpdate { - status: enums::CaptureStatus::foreign_try_from(router_data.status)?, - connector_capture_id: connector_transaction_id.clone(), - connector_response_reference_id, - }; - let capture_update_list = vec![( - multiple_capture_data.get_latest_capture().clone(), - capture_update, - )]; - (Some((multiple_capture_data, capture_update_list)), None) - } - None => ( + let (capture_updates, payment_attempt_update) = match payment_data + .multiple_capture_data + { + Some(multiple_capture_data) => { + let capture_update = storage::CaptureUpdate::ResponseUpdate { + status: enums::CaptureStatus::foreign_try_from(router_data.status)?, + connector_capture_id: connector_transaction_id.clone(), + connector_response_reference_id, + }; + let capture_update_list = vec![( + multiple_capture_data.get_latest_capture().clone(), + capture_update, + )]; + (Some((multiple_capture_data, capture_update_list)), None) + } + None => { + let status = router_data.get_attempt_status_for_db_update(&payment_data); + ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.get_attempt_status_for_db_update(&payment_data), + status, connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, + amount_capturable: router_data + .request + .get_amount_capturable(&payment_data, status), payment_method_id: Some(router_data.payment_method_id), mandate_id: payment_data .mandate_id @@ -632,21 +632,13 @@ async fn payment_response_update_tracker( unified_code: error_status.clone(), unified_message: error_status, connector_response_reference_id, - amount_capturable: if router_data.status.is_terminal_status() - || router_data - .status - .maps_to_intent_status(enums::IntentStatus::Processing) - { - Some(0) - } else { - None - }, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, }), - ), - }; + ) + } + }; (capture_updates, payment_attempt_update) } @@ -900,7 +892,7 @@ fn get_total_amount_captured( } None => { //Non multiple capture - let amount = request.get_capture_amount(payment_data); + let amount = request.get_captured_amount(payment_data); amount_captured.or_else(|| { if router_data_status == enums::AttemptStatus::Charged { amount diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 3521a82a5a87..e236113e6768 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -599,7 +599,17 @@ pub struct AccessTokenRequestData { } pub trait Capturable { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option + where + F: Clone, + { + None + } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + _attempt_status: common_enums::AttemptStatus, + ) -> Option where F: Clone, { @@ -608,7 +618,7 @@ pub trait Capturable { } impl Capturable for PaymentsAuthorizeData { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option where F: Clone, { @@ -618,41 +628,171 @@ impl Capturable for PaymentsAuthorizeData { .map(|surcharge_details| surcharge_details.final_amount); final_amount.or(Some(self.amount)) } + + fn get_amount_capturable( + &self, + payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + match payment_data + .payment_attempt + .capture_method + .unwrap_or_default() + { + common_enums::CaptureMethod::Automatic => { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Processing => Some(0), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + }, + common_enums::CaptureMethod::Manual => Some(payment_data.payment_attempt.get_total_amount()), + // In case of manual multiple, amount capturable must be inferred from all captures. + common_enums::CaptureMethod::ManualMultiple | + // Scheduled capture is not supported as of now + common_enums::CaptureMethod::Scheduled => None, + } + } } impl Capturable for PaymentsCaptureData { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option where F: Clone, { Some(self.amount_to_capture) } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::Processing => Some(0), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } impl Capturable for CompleteAuthorizeData { - fn get_capture_amount(&self, _payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, _payment_data: &PaymentData) -> Option where F: Clone, { Some(self.amount) } + fn get_amount_capturable( + &self, + payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + match payment_data + .payment_attempt + .capture_method + .unwrap_or_default() + { + common_enums::CaptureMethod::Automatic => { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Succeeded| + common_enums::IntentStatus::Failed| + common_enums::IntentStatus::Processing => Some(0), + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + }, + common_enums::CaptureMethod::Manual => Some(payment_data.payment_attempt.get_total_amount()), + // In case of manual multiple, amount capturable must be inferred from all captures. + common_enums::CaptureMethod::ManualMultiple | + // Scheduled capture is not supported as of now + common_enums::CaptureMethod::Scheduled => None, + } + } } impl Capturable for SetupMandateRequestData {} impl Capturable for PaymentsCancelData { - fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, payment_data: &PaymentData) -> Option where F: Clone, { // return previously captured amount payment_data.payment_intent.amount_captured } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + let intent_status = common_enums::IntentStatus::foreign_from(attempt_status); + match intent_status { + common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::PartiallyCaptured => Some(0), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation + | common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} -impl Capturable for PaymentsIncrementalAuthorizationData {} +impl Capturable for PaymentsIncrementalAuthorizationData { + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + _attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + Some(self.total_amount) + } +} impl Capturable for PaymentsSyncData { - fn get_capture_amount(&self, payment_data: &PaymentData) -> Option + fn get_captured_amount(&self, payment_data: &PaymentData) -> Option where F: Clone, { @@ -661,6 +801,20 @@ impl Capturable for PaymentsSyncData { .amount_to_capture .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) } + fn get_amount_capturable( + &self, + _payment_data: &PaymentData, + attempt_status: common_enums::AttemptStatus, + ) -> Option + where + F: Clone, + { + if attempt_status.is_terminal_status() { + Some(0) + } else { + None + } + } } pub struct AddAccessTokenResult { From b53916d61f6b650ace61942ce2ec902ac15a414b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 00:15:38 +0000 Subject: [PATCH 328/443] chore(version): 2024.01.12.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4f17884aae..023e92ced5c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.12.0 + +### Features + +- **connector:** + - [BOA/Cyb] Include merchant metadata in capture and void requests ([#3308](https://github.com/juspay/hyperswitch/pull/3308)) ([`5a5400c`](https://github.com/juspay/hyperswitch/commit/5a5400cf5b539996b2f327c51d4a07b4a86fd1be)) + - [Volt] Add support for refund webhooks ([#3326](https://github.com/juspay/hyperswitch/pull/3326)) ([`e376f68`](https://github.com/juspay/hyperswitch/commit/e376f68c167a289957a4372df108797088ab1f6e)) + - [BOA/CYB] Store AVS response in connector_metadata ([#3271](https://github.com/juspay/hyperswitch/pull/3271)) ([`e75b11e`](https://github.com/juspay/hyperswitch/commit/e75b11e98ac4c8d37c842c8ee0ccf361dcb52793)) +- **euclid_wasm:** Config changes for NMI ([#3329](https://github.com/juspay/hyperswitch/pull/3329)) ([`ed07c5b`](https://github.com/juspay/hyperswitch/commit/ed07c5ba90868a3132ca90d72219db3ba8978232)) +- **outgoingwebhookevent:** Adding api for query to fetch outgoing webhook events log ([#3310](https://github.com/juspay/hyperswitch/pull/3310)) ([`54d44be`](https://github.com/juspay/hyperswitch/commit/54d44bef730c0679f3535f66e89e88139d70ba2e)) +- **payment_link:** Added sdk layout option payment link ([#3207](https://github.com/juspay/hyperswitch/pull/3207)) ([`6117652`](https://github.com/juspay/hyperswitch/commit/61176524ca0c11c605538a1da9a267837193e1ec)) +- **router:** Payment_method block ([#3056](https://github.com/juspay/hyperswitch/pull/3056)) ([`bb09613`](https://github.com/juspay/hyperswitch/commit/bb096138b5937092badd02741fb869ee35e2e3cc)) +- **users:** Invite user without email ([#3328](https://github.com/juspay/hyperswitch/pull/3328)) ([`6a47063`](https://github.com/juspay/hyperswitch/commit/6a4706323c61f3722dc543993c55084dc9ff9850)) +- Feat(connector): [cybersource] Implement 3DS flow for cards ([#3290](https://github.com/juspay/hyperswitch/pull/3290)) ([`6fb3b00`](https://github.com/juspay/hyperswitch/commit/6fb3b00e82d1e3c03dc1c816ffa6353cc7991a53)) +- Add support for card extended bin in payment attempt ([#3312](https://github.com/juspay/hyperswitch/pull/3312)) ([`cc3eefd`](https://github.com/juspay/hyperswitch/commit/cc3eefd317117d761cdcc76804f3510952d4cec2)) + +### Bug Fixes + +- **core:** Surcharge with saved card failure ([#3318](https://github.com/juspay/hyperswitch/pull/3318)) ([`5a1a3da`](https://github.com/juspay/hyperswitch/commit/5a1a3da7502ce9e13546b896477d82719162d5b6)) +- **refund:** Add merchant_connector_id in refund ([#3303](https://github.com/juspay/hyperswitch/pull/3303)) ([`af43b07`](https://github.com/juspay/hyperswitch/commit/af43b07e4394458db478bc16e5fb8d3b0d636a31)) +- **router:** Add config to avoid connector tokenization for `apple pay` `simplified flow` ([#3234](https://github.com/juspay/hyperswitch/pull/3234)) ([`4f9c04b`](https://github.com/juspay/hyperswitch/commit/4f9c04b856761b9c0486abad4c36de191da2c460)) +- Update amount_capturable based on intent_status and payment flow ([#3278](https://github.com/juspay/hyperswitch/pull/3278)) ([`469ea20`](https://github.com/juspay/hyperswitch/commit/469ea20214aa7c1a3b4b86520724c2509ae37b0b)) + +### Refactors + +- **router:** + - Flagged order_details validation to skip validation ([#3116](https://github.com/juspay/hyperswitch/pull/3116)) ([`8626bda`](https://github.com/juspay/hyperswitch/commit/8626bda6d5aa9e7531edc7ea50ed4f30c3b7227a)) + - Restricted list payment method Customer to api-key based ([#3100](https://github.com/juspay/hyperswitch/pull/3100)) ([`9eaebe8`](https://github.com/juspay/hyperswitch/commit/9eaebe8db3d83105ef1e8fc784241e1fb795dd22)) + +### Miscellaneous Tasks + +- Remove connector auth TOML files from `.gitignore` and `.dockerignore` ([#3330](https://github.com/juspay/hyperswitch/pull/3330)) ([`9f6ef3f`](https://github.com/juspay/hyperswitch/commit/9f6ef3f2240052053b5b7df0a13a5503d8141d56)) + +**Full Changelog:** [`2024.01.11.0...2024.01.12.0`](https://github.com/juspay/hyperswitch/compare/2024.01.11.0...2024.01.12.0) + +- - - + ## 2024.01.11.0 ### Features From 57f2cff75e58b0a7811492a1fdb636f59dcefbd0 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:40:49 +0530 Subject: [PATCH 329/443] chore(config): add merchant_secret config for webhooks for cashtocode and volt in wasm dashboard (#3333) --- crates/connector_configs/toml/development.toml | 4 ++-- crates/connector_configs/toml/production.toml | 3 ++- crates/connector_configs/toml/sandbox.toml | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 2d1363f5831e..dfa0a9ec9232 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -639,8 +639,6 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" - - [cashtocode.connector_webhook_details] merchant_secret="Source verification key" @@ -2085,6 +2083,8 @@ api_key = "Username" api_secret = "Password" key1 = "Client ID" key2 = "Client Secret" +[volt.connector_webhook_details] +merchant_secret="Source verification key" [worldline] [[worldline.credit]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index d4261cb0d94d..e837314f6106 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -517,7 +517,8 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" - +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" [cryptopay] [[cryptopay.crypto]] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 41bc954cc90d..47de5cd5d5ff 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -639,6 +639,8 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_webhook_details] +merchant_secret="Source verification key" [checkout] [[checkout.credit]] @@ -2081,6 +2083,8 @@ api_key = "Username" api_secret = "Password" key1 = "Client ID" key2 = "Client Secret" +[volt.connector_webhook_details] +merchant_secret="Source verification key" [worldline] [[worldline.credit]] From f381d86b7c9fa79d632991c74cab53d0181231c6 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Fri, 12 Jan 2024 18:28:57 +0530 Subject: [PATCH 330/443] chore: add api reference for blocklist (#3336) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/blocklist.rs | 9 +- crates/router/src/db/blocklist.rs | 38 +-- crates/router/src/db/blocklist_fingerprint.rs | 14 +- crates/router/src/db/blocklist_lookup.rs | 22 +- crates/router/src/openapi.rs | 9 +- crates/router/src/routes/blocklist.rs | 38 +++ openapi/openapi_spec.json | 221 ++++++++++++++++++ 7 files changed, 319 insertions(+), 32 deletions(-) diff --git a/crates/api_models/src/blocklist.rs b/crates/api_models/src/blocklist.rs index fc838eed5ce6..888b9106cccc 100644 --- a/crates/api_models/src/blocklist.rs +++ b/crates/api_models/src/blocklist.rs @@ -1,7 +1,8 @@ use common_enums::enums; use common_utils::events::ApiEventMetric; +use utoipa::ToSchema; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum BlocklistRequest { CardBin(String), @@ -12,9 +13,10 @@ pub enum BlocklistRequest { pub type AddToBlocklistRequest = BlocklistRequest; pub type DeleteFromBlocklistRequest = BlocklistRequest; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct BlocklistResponse { pub fingerprint_id: String, + #[schema(value_type = BlocklistDataKind)] pub data_kind: enums::BlocklistDataKind, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: time::PrimitiveDateTime, @@ -23,8 +25,9 @@ pub struct BlocklistResponse { pub type AddToBlocklistResponse = BlocklistResponse; pub type DeleteFromBlocklistResponse = BlocklistResponse; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ListBlocklistQuery { + #[schema(value_type = BlocklistDataKind)] pub data_kind: enums::BlocklistDataKind, #[serde(default = "default_list_limit")] pub limit: u16, diff --git a/crates/router/src/db/blocklist.rs b/crates/router/src/db/blocklist.rs index c263bef63c5a..93361552de70 100644 --- a/crates/router/src/db/blocklist.rs +++ b/crates/router/src/db/blocklist.rs @@ -163,41 +163,49 @@ impl BlocklistInterface for KafkaStore { #[instrument(skip_all)] async fn insert_blocklist_entry( &self, - _pm_blocklist: storage::BlocklistNew, + pm_blocklist: storage::BlocklistNew, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store.insert_blocklist_entry(pm_blocklist).await } async fn find_blocklist_entry_by_merchant_id_fingerprint_id( &self, - _merchant_id: &str, - _fingerprint_id: &str, + merchant_id: &str, + fingerprint: &str, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .find_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, fingerprint) + .await } async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( &self, - _merchant_id: &str, - _fingerprint_id: &str, + merchant_id: &str, + fingerprint: &str, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, fingerprint) + .await } async fn list_blocklist_entries_by_merchant_id_data_kind( &self, - _merchant_id: &str, - _data_kind: common_enums::BlocklistDataKind, - _limit: i64, - _offset: i64, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, ) -> CustomResult, errors::StorageError> { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .list_blocklist_entries_by_merchant_id_data_kind(merchant_id, data_kind, limit, offset) + .await } async fn list_blocklist_entries_by_merchant_id( &self, - _merchant_id: &str, + merchant_id: &str, ) -> CustomResult, errors::StorageError> { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .list_blocklist_entries_by_merchant_id(merchant_id) + .await } } diff --git a/crates/router/src/db/blocklist_fingerprint.rs b/crates/router/src/db/blocklist_fingerprint.rs index 9da7c7d8fb2c..d9107d3d1c13 100644 --- a/crates/router/src/db/blocklist_fingerprint.rs +++ b/crates/router/src/db/blocklist_fingerprint.rs @@ -80,16 +80,20 @@ impl BlocklistFingerprintInterface for KafkaStore { #[instrument(skip_all)] async fn insert_blocklist_fingerprint_entry( &self, - _pm_fingerprint_new: storage::BlocklistFingerprintNew, + pm_fingerprint_new: storage::BlocklistFingerprintNew, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .insert_blocklist_fingerprint_entry(pm_fingerprint_new) + .await } async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( &self, - _merchant_id: &str, - _fingerprint_id: &str, + merchant_id: &str, + fingerprint: &str, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id(merchant_id, fingerprint) + .await } } diff --git a/crates/router/src/db/blocklist_lookup.rs b/crates/router/src/db/blocklist_lookup.rs index 0dfd81c8b8a2..f5fb4ea9ed8c 100644 --- a/crates/router/src/db/blocklist_lookup.rs +++ b/crates/router/src/db/blocklist_lookup.rs @@ -102,24 +102,30 @@ impl BlocklistLookupInterface for KafkaStore { #[instrument(skip_all)] async fn insert_blocklist_lookup_entry( &self, - _blocklist_lookup_entry: storage::BlocklistLookupNew, + blocklist_lookup_entry: storage::BlocklistLookupNew, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .insert_blocklist_lookup_entry(blocklist_lookup_entry) + .await } async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( &self, - _merchant_id: &str, - _fingerprint: &str, + merchant_id: &str, + fingerprint: &str, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .find_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, fingerprint) + .await } async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( &self, - _merchant_id: &str, - _fingerprint: &str, + merchant_id: &str, + fingerprint: &str, ) -> CustomResult { - Err(errors::StorageError::KafkaError)? + self.diesel_store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, fingerprint) + .await } } diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 79b38e03f31d..174926c7d360 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -119,6 +119,9 @@ Never share your secret api keys. Keep them guarded and secure. crate::routes::gsm::get_gsm_rule, crate::routes::gsm::update_gsm_rule, crate::routes::gsm::delete_gsm_rule, + crate::routes::blocklist::add_entry_to_blocklist, + crate::routes::blocklist::list_blocked_payment_methods, + crate::routes::blocklist::remove_entry_from_blocklist ), components(schemas( crate::types::api::refunds::RefundRequest, @@ -370,7 +373,11 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkStatus + api_models::payments::PaymentLinkStatus, + api_models::blocklist::BlocklistRequest, + api_models::blocklist::BlocklistResponse, + api_models::blocklist::ListBlocklistQuery, + common_enums::enums::BlocklistDataKind )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/routes/blocklist.rs b/crates/router/src/routes/blocklist.rs index 7c268dddeec0..9c93f49ab83f 100644 --- a/crates/router/src/routes/blocklist.rs +++ b/crates/router/src/routes/blocklist.rs @@ -8,6 +8,18 @@ use crate::{ services::{api, authentication as auth, authorization::permissions::Permission}, }; +#[utoipa::path( + post, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Blocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Block a Fingerprint", + security(("api_key" = [])) +)] pub async fn add_entry_to_blocklist( state: web::Data, req: HttpRequest, @@ -32,6 +44,18 @@ pub async fn add_entry_to_blocklist( .await } +#[utoipa::path( + delete, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Unblocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Unblock a Fingerprint", + security(("api_key" = [])) +)] pub async fn remove_entry_from_blocklist( state: web::Data, req: HttpRequest, @@ -56,6 +80,20 @@ pub async fn remove_entry_from_blocklist( .await } +#[utoipa::path( + get, + path = "/blocklist", + params ( + ("data_kind" = BlocklistDataKind, Query, description = "Kind of the fingerprint list requested"), + ), + responses( + (status = 200, description = "Blocked Fingerprints", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "List Blocked fingerprints of a particular kind", + security(("api_key" = [])) +)] pub async fn list_blocked_payment_methods( state: web::Data, req: HttpRequest, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 3e582cfed528..c50f687a1810 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -382,6 +382,117 @@ ] } }, + "/blocklist": { + "get": { + "tags": [ + "Blocklist" + ], + "operationId": "List Blocked fingerprints of a particular kind", + "parameters": [ + { + "name": "data_kind", + "in": "query", + "description": "Kind of the fingerprint list requested", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlocklistDataKind" + } + } + ], + "responses": { + "200": { + "description": "Blocked Fingerprints", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Blocklist" + ], + "operationId": "Block a Fingerprint", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Fingerprint Blocked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Blocklist" + ], + "operationId": "Unblock a Fingerprint", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Fingerprint Unblocked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlocklistResponse" + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/customers": { "post": { "tags": [ @@ -4035,6 +4146,95 @@ } ] }, + "BlocklistDataKind": { + "type": "string", + "enum": [ + "payment_method", + "card_bin", + "extended_card_bin" + ] + }, + "BlocklistRequest": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "card_bin" + ] + }, + "data": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fingerprint" + ] + }, + "data": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "extended_card_bin" + ] + }, + "data": { + "type": "string" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "BlocklistResponse": { + "type": "object", + "required": [ + "fingerprint_id", + "data_kind", + "created_at" + ], + "properties": { + "fingerprint_id": { + "type": "string" + }, + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, "BoletoVoucherData": { "type": "object", "properties": { @@ -6576,6 +6776,27 @@ } } }, + "ListBlocklistQuery": { + "type": "object", + "required": [ + "data_kind" + ], + "properties": { + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" + }, + "limit": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "offset": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, "MandateAmountData": { "type": "object", "required": [ From 1bbd9d5df0f145f192d0271d89761488e7347989 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:17:52 +0000 Subject: [PATCH 331/443] chore(version): 2024.01.12.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 023e92ced5c0..739b8cd2c667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.12.1 + +### Miscellaneous Tasks + +- **config:** Add merchant_secret config for webhooks for cashtocode and volt in wasm dashboard ([#3333](https://github.com/juspay/hyperswitch/pull/3333)) ([`57f2cff`](https://github.com/juspay/hyperswitch/commit/57f2cff75e58b0a7811492a1fdb636f59dcefbd0)) +- Add api reference for blocklist ([#3336](https://github.com/juspay/hyperswitch/pull/3336)) ([`f381d86`](https://github.com/juspay/hyperswitch/commit/f381d86b7c9fa79d632991c74cab53d0181231c6)) + +**Full Changelog:** [`2024.01.12.0...2024.01.12.1`](https://github.com/juspay/hyperswitch/compare/2024.01.12.0...2024.01.12.1) + +- - - + ## 2024.01.12.0 ### Features From 58cc8d6109ce49d385b06c762ab3f6670f5094eb Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:06:47 +0530 Subject: [PATCH 332/443] fix(connector_onboarding): Check if connector exists for the merchant account and add reset tracking id API (#3229) --- crates/api_models/src/connector_onboarding.rs | 6 + .../src/events/connector_onboarding.rs | 4 +- crates/router/src/consts.rs | 3 + .../router/src/core/connector_onboarding.rs | 44 +++++-- .../src/core/connector_onboarding/paypal.rs | 20 ++-- crates/router/src/routes/app.rs | 1 + .../router/src/routes/connector_onboarding.rs | 21 +++- crates/router/src/routes/lock_utils.rs | 4 +- .../router/src/utils/connector_onboarding.rs | 109 +++++++++++++++++- crates/router_env/src/logger/types.rs | 2 + 10 files changed, 186 insertions(+), 28 deletions(-) diff --git a/crates/api_models/src/connector_onboarding.rs b/crates/api_models/src/connector_onboarding.rs index 759d3cb97f13..7e8288d9747f 100644 --- a/crates/api_models/src/connector_onboarding.rs +++ b/crates/api_models/src/connector_onboarding.rs @@ -52,3 +52,9 @@ pub struct PayPalOnboardingDone { pub struct PayPalIntegrationDone { pub connector_id: String, } + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct ResetTrackingIdRequest { + pub connector_id: String, + pub connector: enums::Connector, +} diff --git a/crates/api_models/src/events/connector_onboarding.rs b/crates/api_models/src/events/connector_onboarding.rs index 998dc384d620..0da89f61da7e 100644 --- a/crates/api_models/src/events/connector_onboarding.rs +++ b/crates/api_models/src/events/connector_onboarding.rs @@ -2,11 +2,13 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::connector_onboarding::{ ActionUrlRequest, ActionUrlResponse, OnboardingStatus, OnboardingSyncRequest, + ResetTrackingIdRequest, }; common_utils::impl_misc_api_event_type!( ActionUrlRequest, ActionUrlResponse, OnboardingSyncRequest, - OnboardingStatus + OnboardingStatus, + ResetTrackingIdRequest ); diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index ed020b0c7e0f..387da3c06415 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -77,6 +77,9 @@ pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_MERCHANT_ID: &str = "test_merchant"; +#[cfg(feature = "olap")] +pub const CONNECTOR_ONBOARDING_CONFIG_PREFIX: &str = "onboarding"; + /// Max payment session expiry pub const MAX_SESSION_EXPIRY: u32 = 7890000; diff --git a/crates/router/src/core/connector_onboarding.rs b/crates/router/src/core/connector_onboarding.rs index e48026edc2d5..e6c1fc9d378d 100644 --- a/crates/router/src/core/connector_onboarding.rs +++ b/crates/router/src/core/connector_onboarding.rs @@ -1,5 +1,4 @@ use api_models::{connector_onboarding as api, enums}; -use error_stack::ResultExt; use masking::Secret; use crate::{ @@ -19,16 +18,23 @@ pub trait AccessToken { pub async fn get_action_url( state: AppState, + user_from_token: auth::UserFromToken, request: api::ActionUrlRequest, ) -> RouterResponse { + utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id) + .await?; + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + let tracking_id = + utils::get_tracking_id_from_configs(&state, &request.connector_id, request.connector) + .await?; match (is_enabled, request.connector) { (Some(true), enums::Connector::Paypal) => { let action_url = Box::pin(paypal::get_action_url_from_paypal( state, - request.connector_id, + tracking_id, request.return_url, )) .await?; @@ -49,40 +55,42 @@ pub async fn sync_onboarding_status( user_from_token: auth::UserFromToken, request: api::OnboardingSyncRequest, ) -> RouterResponse { - let merchant_account = user_from_token - .get_merchant_account(state.clone()) - .await - .change_context(ApiErrorResponse::MerchantAccountNotFound)?; + utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id) + .await?; + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + let tracking_id = + utils::get_tracking_id_from_configs(&state, &request.connector_id, request.connector) + .await?; match (is_enabled, request.connector) { (Some(true), enums::Connector::Paypal) => { let status = Box::pin(paypal::sync_merchant_onboarding_status( state.clone(), - request.connector_id.clone(), + tracking_id, )) .await?; if let api::OnboardingStatus::PayPal(api::PayPalOnboardingStatus::Success( - ref inner_data, + ref paypal_onboarding_data, )) = status { let connector_onboarding_conf = state.conf.connector_onboarding.clone(); let auth_details = oss_types::ConnectorAuthType::SignatureKey { api_key: connector_onboarding_conf.paypal.client_secret, key1: connector_onboarding_conf.paypal.client_id, - api_secret: Secret::new(inner_data.payer_id.clone()), + api_secret: Secret::new(paypal_onboarding_data.payer_id.clone()), }; - let some_data = paypal::update_mca( + let update_mca_data = paypal::update_mca( &state, - &merchant_account, + user_from_token.merchant_id, request.connector_id.to_owned(), auth_details, ) .await?; return Ok(ApplicationResponse::Json(api::OnboardingStatus::PayPal( - api::PayPalOnboardingStatus::ConnectorIntegrated(some_data), + api::PayPalOnboardingStatus::ConnectorIntegrated(update_mca_data), ))); } Ok(ApplicationResponse::Json(status)) @@ -94,3 +102,15 @@ pub async fn sync_onboarding_status( .into()), } } + +pub async fn reset_tracking_id( + state: AppState, + user_from_token: auth::UserFromToken, + request: api::ResetTrackingIdRequest, +) -> RouterResponse<()> { + utils::check_if_connector_exists(&state, &request.connector_id, &user_from_token.merchant_id) + .await?; + utils::set_tracking_id_in_configs(&state, &request.connector_id, request.connector).await?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/connector_onboarding/paypal.rs b/crates/router/src/core/connector_onboarding/paypal.rs index 30aa69067b5d..f18681f8cfdb 100644 --- a/crates/router/src/core/connector_onboarding/paypal.rs +++ b/crates/router/src/core/connector_onboarding/paypal.rs @@ -23,11 +23,11 @@ fn build_referral_url(state: AppState) -> String { async fn build_referral_request( state: AppState, - connector_id: String, + tracking_id: String, return_url: String, ) -> RouterResult { let access_token = utils::paypal::generate_access_token(state.clone()).await?; - let request_body = types::paypal::PartnerReferralRequest::new(connector_id, return_url); + let request_body = types::paypal::PartnerReferralRequest::new(tracking_id, return_url); utils::paypal::build_paypal_post_request( build_referral_url(state), @@ -38,12 +38,12 @@ async fn build_referral_request( pub async fn get_action_url_from_paypal( state: AppState, - connector_id: String, + tracking_id: String, return_url: String, ) -> RouterResult { let referral_request = Box::pin(build_referral_request( state.clone(), - connector_id, + tracking_id, return_url, )) .await?; @@ -137,7 +137,7 @@ async fn find_paypal_merchant_by_tracking_id( pub async fn update_mca( state: &AppState, - merchant_account: &oss_types::domain::MerchantAccount, + merchant_id: String, connector_id: String, auth_details: oss_types::ConnectorAuthType, ) -> RouterResult { @@ -159,13 +159,9 @@ pub async fn update_mca( connector_webhook_details: None, pm_auth_config: None, }; - let mca_response = admin::update_payment_connector( - state.clone(), - &merchant_account.merchant_id, - &connector_id, - request, - ) - .await?; + let mca_response = + admin::update_payment_connector(state.clone(), &merchant_id, &connector_id, request) + .await?; match mca_response { ApplicationResponse::Json(mca_data) => Ok(mca_data), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0b2acaf4e506..77253d1d75c4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -961,5 +961,6 @@ impl ConnectorOnboarding { .app_data(web::Data::new(state)) .service(web::resource("/action_url").route(web::post().to(get_action_url))) .service(web::resource("/sync").route(web::post().to(sync_onboarding_status))) + .service(web::resource("/reset_tracking_id").route(web::post().to(reset_tracking_id))) } } diff --git a/crates/router/src/routes/connector_onboarding.rs b/crates/router/src/routes/connector_onboarding.rs index b7c39b3c1d2e..f5555f5bf9bf 100644 --- a/crates/router/src/routes/connector_onboarding.rs +++ b/crates/router/src/routes/connector_onboarding.rs @@ -20,7 +20,7 @@ pub async fn get_action_url( state, &http_req, req_payload.clone(), - |state, _: auth::UserFromToken, req| core::get_action_url(state, req), + core::get_action_url, &auth::JWTAuth(Permission::MerchantAccountWrite), api_locking::LockAction::NotApplicable, )) @@ -45,3 +45,22 @@ pub async fn sync_onboarding_status( )) .await } + +pub async fn reset_tracking_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ResetTrackingId; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + core::reset_tracking_id, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 55c6cbc23d70..c560f0d988af 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -183,7 +183,9 @@ impl From for ApiIdentifier { Self::UserRole } - Flow::GetActionUrl | Flow::SyncOnboardingStatus => Self::ConnectorOnboarding, + Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { + Self::ConnectorOnboarding + } } } } diff --git a/crates/router/src/utils/connector_onboarding.rs b/crates/router/src/utils/connector_onboarding.rs index e8afcd68a468..03735e61cc70 100644 --- a/crates/router/src/utils/connector_onboarding.rs +++ b/crates/router/src/utils/connector_onboarding.rs @@ -1,6 +1,11 @@ +use diesel_models::{ConfigNew, ConfigUpdate}; +use error_stack::ResultExt; + +use super::errors::StorageErrorExt; use crate::{ + consts, core::errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, - routes::app::settings, + routes::{app::settings, AppState}, types::{self, api::enums}, }; @@ -34,3 +39,105 @@ pub fn is_enabled( _ => None, } } + +pub async fn check_if_connector_exists( + state: &AppState, + connector_id: &str, + merchant_id: &str, +) -> RouterResult<()> { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(ApiErrorResponse::MerchantAccountNotFound)?; + + let _connector = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + merchant_id, + connector_id, + &key_store, + ) + .await + .to_not_found_response(ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.to_string(), + })?; + + Ok(()) +} + +pub async fn set_tracking_id_in_configs( + state: &AppState, + connector_id: &str, + connector: enums::Connector, +) -> RouterResult<()> { + let timestamp = common_utils::date_time::now_unix_timestamp().to_string(); + let find_config = state + .store + .find_config_by_key(&build_key(connector_id, connector)) + .await; + + if find_config.is_ok() { + state + .store + .update_config_by_key( + &build_key(connector_id, connector), + ConfigUpdate::Update { + config: Some(timestamp), + }, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error updating data in configs table")?; + } else if find_config + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + state + .store + .insert_config(ConfigNew { + key: build_key(connector_id, connector), + config: timestamp, + }) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error inserting data in configs table")?; + } else { + find_config.change_context(ApiErrorResponse::InternalServerError)?; + } + + Ok(()) +} + +pub async fn get_tracking_id_from_configs( + state: &AppState, + connector_id: &str, + connector: enums::Connector, +) -> RouterResult { + let timestamp = state + .store + .find_config_by_key_unwrap_or( + &build_key(connector_id, connector), + Some(common_utils::date_time::now_unix_timestamp().to_string()), + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error getting data from configs table")? + .config; + + Ok(format!("{}_{}", connector_id, timestamp)) +} + +fn build_key(connector_id: &str, connector: enums::Connector) -> String { + format!( + "{}_{}_{}", + consts::CONNECTOR_ONBOARDING_CONFIG_PREFIX, + connector, + connector_id, + ) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index a6ac1b1e0a14..8f0b9bad3e80 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -317,6 +317,8 @@ pub enum Flow { GetActionUrl, /// Sync connector onboarding status SyncOnboardingStatus, + /// Reset tracking id + ResetTrackingId, /// Verify email Token VerifyEmail, /// Send verify email From 5ad3f8939afafce3eec39704dcaa92270b384dcd Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Tue, 16 Jan 2024 13:43:19 +0530 Subject: [PATCH 333/443] fix(payment_link): added expires_on in payment response (#3332) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 5 +++++ crates/router/src/core/payments/transformers.rs | 2 ++ openapi/openapi_spec.json | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index cac94a07326a..06bd229586d9 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2277,6 +2277,11 @@ pub struct PaymentsResponse { /// List of incremental authorizations happened to the payment pub incremental_authorizations: Option>, + /// Date Time expiry of the payment + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub expires_on: Option, + /// Payment Fingerprint pub fingerprint: Option, } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index dffcff23595b..5ab6bffc8e6d 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -709,6 +709,7 @@ where .set_fingerprint(payment_intent.fingerprint_id) .set_authorization_count(payment_intent.authorization_count) .set_incremental_authorizations(incremental_authorizations_response) + .set_expires_on(payment_intent.session_expiry) .to_owned(), headers, )) @@ -775,6 +776,7 @@ where incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, authorization_count: payment_intent.authorization_count, incremental_authorizations: incremental_authorizations_response, + expires_on: payment_intent.session_expiry, ..Default::default() }, headers, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index c50f687a1810..466489e2f9f9 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10948,6 +10948,13 @@ "description": "List of incremental authorizations happened to the payment", "nullable": true }, + "expires_on": { + "type": "string", + "format": "date-time", + "description": "Date Time expiry of the payment", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, "fingerprint": { "type": "string", "description": "Payment Fingerprint", From 8678f8d1448b5ce430931bfbbc269ef979d9eea7 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:37:44 +0530 Subject: [PATCH 334/443] feat(recon): add recon APIs (#3345) Co-authored-by: Kashif Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/Cargo.toml | 3 +- crates/api_models/src/events.rs | 2 + crates/api_models/src/events/recon.rs | 21 ++ crates/api_models/src/events/user.rs | 14 + crates/api_models/src/lib.rs | 2 + crates/api_models/src/recon.rs | 21 ++ crates/api_models/src/user.rs | 7 + crates/common_utils/src/events.rs | 1 + crates/router/Cargo.toml | 3 +- crates/router/src/core/user.rs | 29 ++ crates/router/src/lib.rs | 6 + crates/router/src/routes.rs | 4 + crates/router/src/routes/app.rs | 22 ++ crates/router/src/routes/lock_utils.rs | 6 + crates/router/src/routes/recon.rs | 250 ++++++++++++++++++ crates/router/src/routes/user.rs | 15 ++ crates/router/src/services.rs | 2 + crates/router/src/services/authentication.rs | 96 +++++++ ...n_activated.html => recon_activation.html} | 0 crates/router/src/services/email/types.rs | 105 +++++++- crates/router/src/services/recon.rs | 29 ++ crates/router_env/src/logger/types.rs | 8 + 22 files changed, 639 insertions(+), 7 deletions(-) create mode 100644 crates/api_models/src/events/recon.rs create mode 100644 crates/api_models/src/recon.rs create mode 100644 crates/router/src/routes/recon.rs rename crates/router/src/services/email/assets/{recon_activated.html => recon_activation.html} (100%) create mode 100644 crates/router/src/services/recon.rs diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 69980361500c..45702a4ecb0a 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -default = ["payouts", "frm"] +default = ["payouts", "frm", "recon"] business_profile_routing = [] connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] @@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] frm = [] +recon = [] [dependencies] actix-web = { version = "4.3.1", optional = true } diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 6d9bd5db3429..26a9d222d6b9 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -5,6 +5,8 @@ mod locker_migration; pub mod payment; #[cfg(feature = "payouts")] pub mod payouts; +#[cfg(feature = "recon")] +pub mod recon; pub mod refund; pub mod routing; pub mod user; diff --git a/crates/api_models/src/events/recon.rs b/crates/api_models/src/events/recon.rs new file mode 100644 index 000000000000..aed648f4c869 --- /dev/null +++ b/crates/api_models/src/events/recon.rs @@ -0,0 +1,21 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::recon::{ReconStatusResponse, ReconTokenResponse, ReconUpdateMerchantRequest}; + +impl ApiEventMetric for ReconUpdateMerchantRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Recon) + } +} + +impl ApiEventMetric for ReconTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Recon) + } +} + +impl ApiEventMetric for ReconStatusResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Recon) + } +} diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 1f4cb7359c79..c0743c8b8fc0 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,7 +1,11 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; +#[cfg(feature = "recon")] +use masking::PeekInterface; #[cfg(feature = "dummy_connector")] use crate::user::sample_data::SampleDataRequest; +#[cfg(feature = "recon")] +use crate::user::VerifyTokenResponse; use crate::user::{ dashboard_metadata::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, @@ -21,6 +25,16 @@ impl ApiEventMetric for DashboardEntryResponse { } } +#[cfg(feature = "recon")] +impl ApiEventMetric for VerifyTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::User { + merchant_id: self.merchant_id.clone(), + user_id: self.user_email.peek().to_string(), + }) + } +} + common_utils::impl_misc_api_event_type!( SignUpRequest, SignUpWithMerchantIdRequest, diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index dc1f6eb65375..1ea79ff6fe8f 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -26,6 +26,8 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod pm_auth; +#[cfg(feature = "recon")] +pub mod recon; pub mod refunds; pub mod routing; pub mod surcharge_decision_configs; diff --git a/crates/api_models/src/recon.rs b/crates/api_models/src/recon.rs new file mode 100644 index 000000000000..efbe28f96ba4 --- /dev/null +++ b/crates/api_models/src/recon.rs @@ -0,0 +1,21 @@ +use common_utils::pii; +use masking::Secret; + +use crate::enums; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ReconUpdateMerchantRequest { + pub merchant_id: String, + pub recon_status: enums::ReconStatus, + pub user_email: pii::Email, +} + +#[derive(Debug, serde::Serialize)] +pub struct ReconTokenResponse { + pub token: Secret, +} + +#[derive(Debug, serde::Serialize)] +pub struct ReconStatusResponse { + pub recon_status: enums::ReconStatus, +} diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index f5af31c8e7f6..a04c4fef6601 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -140,3 +140,10 @@ pub struct UserMerchantAccount { pub merchant_id: String, pub merchant_name: OptionalEncryptableName, } + +#[cfg(feature = "recon")] +#[derive(serde::Serialize, Debug)] +pub struct VerifyTokenResponse { + pub merchant_id: String, + pub user_email: pii::Email, +} diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 6bbf78afe421..c2bf50d96c31 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -49,6 +49,7 @@ pub enum ApiEventsType { Miscellaneous, RustLocker, FraudCheck, + Recon, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 8ecac3620919..0a544e0bd090 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "recon"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] @@ -30,6 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] +recon = ["email"] retry = [] [dependencies] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index b1a582cedecf..27a4f67618e4 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -757,3 +757,32 @@ pub async fn send_verification_mail( Ok(ApplicationResponse::StatusOk) } + +#[cfg(feature = "recon")] +pub async fn verify_token( + state: AppState, + req: auth::ReconUser, +) -> UserResponse { + let user = state + .store + .find_user_by_id(&req.user_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + let merchant_id = state + .store + .find_user_role_by_user_id(&req.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .merchant_id; + + Ok(ApplicationResponse::Json(user_api::VerifyTokenResponse { + merchant_id: merchant_id.to_string(), + user_email: user.email, + })) +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 696198f2153c..c38a4dc85b55 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -165,6 +165,12 @@ pub fn mk_app( { server_app = server_app.service(routes::StripeApis::server(state.clone())); } + + #[cfg(feature = "recon")] + { + server_app = server_app.service(routes::Recon::server(state.clone())); + } + server_app = server_app.service(routes::Cards::server(state.clone())); server_app = server_app.service(routes::Cache::server(state.clone())); server_app = server_app.service(routes::Health::server(state)); diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index d4bfabb6f92a..d9916f98e745 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -28,6 +28,8 @@ pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; +#[cfg(feature = "recon")] +pub mod recon; pub mod refunds; #[cfg(feature = "olap")] pub mod routing; @@ -53,6 +55,8 @@ pub use self::app::DummyConnector; pub use self::app::Forex; #[cfg(feature = "payouts")] pub use self::app::Payouts; +#[cfg(all(feature = "olap", feature = "recon"))] +pub use self::app::Recon; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 77253d1d75c4..0c489dbe63a7 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -40,6 +40,8 @@ use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; #[cfg(all(feature = "frm", feature = "oltp"))] use crate::routes::fraud_check as frm_routes; +#[cfg(all(feature = "recon", feature = "olap"))] +use crate::routes::recon as recon_routes; #[cfg(feature = "olap")] use crate::routes::verify_connector::payment_connector_verify; pub use crate::{ @@ -568,6 +570,26 @@ impl PaymentMethods { } } +#[cfg(all(feature = "olap", feature = "recon"))] +pub struct Recon; + +#[cfg(all(feature = "olap", feature = "recon"))] +impl Recon { + pub fn server(state: AppState) -> Scope { + web::scope("/recon") + .app_data(web::Data::new(state)) + .service( + web::resource("/update_merchant") + .route(web::post().to(recon_routes::update_merchant)), + ) + .service(web::resource("/token").route(web::get().to(recon_routes::get_recon_token))) + .service( + web::resource("/request").route(web::post().to(recon_routes::request_for_recon)), + ) + .service(web::resource("/verify_token").route(web::get().to(verify_recon_token))) + } +} + #[cfg(feature = "olap")] pub struct Blocklist; diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index c560f0d988af..12cf76be4759 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -31,6 +31,7 @@ pub enum ApiIdentifier { User, UserRole, ConnectorOnboarding, + Recon, } impl From for ApiIdentifier { @@ -186,6 +187,11 @@ impl From for ApiIdentifier { Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { Self::ConnectorOnboarding } + + Flow::ReconMerchantUpdate + | Flow::ReconTokenRequest + | Flow::ReconServiceRequest + | Flow::ReconVerifyToken => Self::Recon, } } } diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs new file mode 100644 index 000000000000..d34e30237ddc --- /dev/null +++ b/crates/router/src/routes/recon.rs @@ -0,0 +1,250 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::recon as recon_api; +use common_enums::ReconStatus; +use error_stack::ResultExt; +use masking::{ExposeInterface, PeekInterface, Secret}; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{ + api_locking, + errors::{self, RouterResponse, RouterResult, StorageErrorExt, UserErrors}, + }, + services::{ + api as service_api, api, + authentication::{self as auth, ReconUser, UserFromToken}, + email::types as email_types, + recon::ReconToken, + }, + types::{ + api::{self as api_types, enums}, + domain::{UserEmail, UserFromStorage, UserName}, + storage, + }, +}; + +pub async fn update_merchant( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ReconMerchantUpdate; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, _user, req| recon_merchant_account_update(state, req), + &auth::ReconAdmin, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn request_for_recon(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconServiceRequest; + Box::pin(api::server_wrap( + flow, + state, + &http_req, + (), + |state, user: UserFromToken, _req| send_recon_request(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_recon_token(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconTokenRequest; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, user: ReconUser, _| generate_recon_token(state, user), + &auth::ReconJWT, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn send_recon_request( + state: AppState, + user: UserFromToken, +) -> RouterResponse { + let db = &*state.store; + let user_from_db = db + .find_user_by_id(&user.user_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let merchant_id = db + .find_user_role_by_user_id(&user.user_id) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)? + .merchant_id; + let key_store = db + .get_merchant_key_store_by_merchant_id( + merchant_id.as_str(), + &db.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id.as_str(), &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let email_contents = email_types::ProFeatureRequest { + feature_name: "RECONCILIATION & SETTLEMENT".to_string(), + merchant_id: merchant_id.clone(), + user_name: UserName::new(user_from_db.name) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form username")?, + recipient_email: UserEmail::from_pii_email(user_from_db.email.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert to UserEmail from pii::Email")?, + settings: state.conf.clone(), + subject: format!( + "Dashboard Pro Feature Request by {}", + user_from_db.email.expose().peek() + ), + }; + + let is_email_sent = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to compose and send email for ProFeatureRequest") + .is_ok(); + + if is_email_sent { + let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: enums::ReconStatus::Requested, + }; + + let response = db + .update_merchant(merchant_account, updated_merchant_account, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id}") + })?; + + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconStatusResponse { + recon_status: response.recon_status, + }, + )) + } else { + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconStatusResponse { + recon_status: enums::ReconStatus::NotRequested, + }, + )) + } +} + +pub async fn recon_merchant_account_update( + state: AppState, + req: recon_api::ReconUpdateMerchantRequest, +) -> RouterResponse { + let merchant_id = &req.merchant_id.clone(); + let user_email = &req.user_email.clone(); + + let db = &*state.store; + + let key_store = db + .get_merchant_key_store_by_merchant_id( + &req.merchant_id, + &db.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let updated_merchant_account = storage::MerchantAccountUpdate::ReconUpdate { + recon_status: req.recon_status, + }; + + let response = db + .update_merchant(merchant_account, updated_merchant_account, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!("Failed while updating merchant's recon status: {merchant_id}") + })?; + + let email_contents = email_types::ReconActivation { + recipient_email: UserEmail::from_pii_email(user_email.clone()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to convert to UserEmail from pii::Email")?, + user_name: UserName::new(Secret::new("HyperSwitch User".to_string())) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to form username")?, + settings: state.conf.clone(), + subject: "Approval of Recon Request - Access Granted to Recon Dashboard", + }; + + if req.recon_status == ReconStatus::Active { + let _is_email_sent = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to compose and send email for ReconActivation") + .is_ok(); + } + + Ok(service_api::ApplicationResponse::Json( + response + .try_into() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_account", + })?, + )) +} + +pub async fn generate_recon_token( + state: AppState, + req: ReconUser, +) -> RouterResponse { + let db = &*state.store; + let user = db + .find_user_by_id(&req.user_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(errors::ApiErrorResponse::InvalidJwtToken) + } else { + e.change_context(errors::ApiErrorResponse::InternalServerError) + } + })? + .into(); + + let token = Box::pin(get_recon_auth_token(user, state)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + Ok(service_api::ApplicationResponse::Json( + recon_api::ReconTokenResponse { token }, + )) +} + +pub async fn get_recon_auth_token( + user: UserFromStorage, + state: AppState, +) -> RouterResult> { + ReconToken::new_token(user.0.user_id.clone(), &state.conf).await +} diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index a77b82c550e6..976fd5c9f564 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -388,3 +388,18 @@ pub async fn verify_email_request( )) .await } + +#[cfg(feature = "recon")] +pub async fn verify_recon_token(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::ReconVerifyToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, user, _req| user_core::verify_token(state, user), + &auth::ReconJWT, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 57f3b802bd5d..8c973105d53b 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -7,6 +7,8 @@ pub mod jwt; pub mod kafka; pub mod logger; pub mod pm_auth; +#[cfg(feature = "recon")] +pub mod recon; #[cfg(feature = "email")] pub mod email; diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index b48465ebd174..3370912394e0 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -12,10 +12,14 @@ use serde::Serialize; use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; +#[cfg(feature = "recon")] +use super::recon::ReconToken; #[cfg(feature = "olap")] use crate::consts; #[cfg(feature = "olap")] use crate::core::errors::UserResult; +#[cfg(feature = "recon")] +use crate::routes::AppState; use crate::{ configs::settings, core::{ @@ -822,3 +826,95 @@ where } default_auth } + +#[cfg(feature = "recon")] +static RECON_API_KEY: tokio::sync::OnceCell> = + tokio::sync::OnceCell::const_new(); + +#[cfg(feature = "recon")] +pub async fn get_recon_admin_api_key( + secrets: &settings::Secrets, + #[cfg(feature = "kms")] kms_client: &kms::KmsClient, +) -> RouterResult<&'static StrongSecret> { + RECON_API_KEY + .get_or_try_init(|| async { + #[cfg(feature = "kms")] + let recon_admin_api_key = secrets + .kms_encrypted_recon_admin_api_key + .decrypt_inner(kms_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to KMS decrypt recon admin API key")?; + + #[cfg(not(feature = "kms"))] + let recon_admin_api_key = secrets.recon_admin_api_key.clone(); + + Ok(StrongSecret::new(recon_admin_api_key)) + }) + .await +} + +#[cfg(feature = "recon")] +pub struct ReconAdmin; + +#[async_trait] +#[cfg(feature = "recon")] +impl AuthenticateAndFetch<(), A> for ReconAdmin +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + let request_admin_api_key = + get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; + let conf = state.conf(); + + let admin_api_key = get_recon_admin_api_key( + &conf.secrets, + #[cfg(feature = "kms")] + kms::get_kms_client(&conf.kms).await, + ) + .await?; + + if request_admin_api_key != admin_api_key.peek() { + Err(report!(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Recon Admin Authentication Failure"))?; + } + + Ok(((), AuthenticationType::NoAuth)) + } +} + +#[cfg(feature = "recon")] +pub struct ReconJWT; +#[cfg(feature = "recon")] +pub struct ReconUser { + pub user_id: String, +} +#[cfg(feature = "recon")] +impl AuthInfo for ReconUser { + fn get_merchant_id(&self) -> Option<&str> { + None + } +} +#[cfg(all(feature = "olap", feature = "recon"))] +#[async_trait] +impl AuthenticateAndFetch for ReconJWT { + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &AppState, + ) -> RouterResult<(ReconUser, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + ReconUser { + user_id: payload.user_id, + }, + AuthenticationType::NoAuth, + )) + } +} diff --git a/crates/router/src/services/email/assets/recon_activated.html b/crates/router/src/services/email/assets/recon_activation.html similarity index 100% rename from crates/router/src/services/email/assets/recon_activated.html rename to crates/router/src/services/email/assets/recon_activation.html diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index d5c28b1fd6af..0ef15eaa40d2 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -1,17 +1,37 @@ use common_utils::errors::CustomResult; use error_stack::ResultExt; use external_services::email::{EmailContents, EmailData, EmailError}; -use masking::ExposeInterface; +use masking::{ExposeInterface, PeekInterface}; use crate::{configs, consts}; #[cfg(feature = "olap")] use crate::{core::errors::UserErrors, services::jwt, types::domain}; pub enum EmailBody { - Verify { link: String }, - Reset { link: String, user_name: String }, - MagicLink { link: String, user_name: String }, - InviteUser { link: String, user_name: String }, + Verify { + link: String, + }, + Reset { + link: String, + user_name: String, + }, + MagicLink { + link: String, + user_name: String, + }, + InviteUser { + link: String, + user_name: String, + }, + ReconActivation { + user_name: String, + }, + ProFeatureRequest { + feature_name: String, + merchant_id: String, + user_name: String, + user_email: String, + }, } pub mod html { @@ -43,6 +63,30 @@ pub mod html { link = link ) } + EmailBody::ReconActivation { user_name } => { + format!( + include_str!("assets/recon_activation.html"), + username = user_name, + ) + } + EmailBody::ProFeatureRequest { + feature_name, + merchant_id, + user_name, + user_email, + } => { + format!( + "Dear Hyperswitch Support Team, + + Dashboard Pro Feature Request, + Feature name : {feature_name} + Merchant ID : {merchant_id} + Merchant Name : {user_name} + Email : {user_email} + + (note: This is an auto generated email. use merchant email for any further comunications)", + ) + } } } } @@ -198,3 +242,54 @@ impl EmailData for InviteUser { }) } } + +pub struct ReconActivation { + pub recipient_email: domain::UserEmail, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +#[async_trait::async_trait] +impl EmailData for ReconActivation { + async fn get_email_data(&self) -> CustomResult { + let body = html::get_html_body(EmailBody::ReconActivation { + user_name: self.user_name.clone().get_secret().expose(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + +pub struct ProFeatureRequest { + pub recipient_email: domain::UserEmail, + pub feature_name: String, + pub merchant_id: String, + pub user_name: domain::UserName, + pub settings: std::sync::Arc, + pub subject: String, +} + +#[async_trait::async_trait] +impl EmailData for ProFeatureRequest { + async fn get_email_data(&self) -> CustomResult { + let recipient = self.recipient_email.clone().into_inner(); + + let body = html::get_html_body(EmailBody::ProFeatureRequest { + user_name: self.user_name.clone().get_secret().expose(), + feature_name: self.feature_name.clone(), + merchant_id: self.merchant_id.clone(), + user_email: recipient.peek().to_string(), + }); + + Ok(EmailContents { + subject: self.subject.clone(), + body: external_services::email::IntermediateString::new(body), + recipient, + }) + } +} diff --git a/crates/router/src/services/recon.rs b/crates/router/src/services/recon.rs new file mode 100644 index 000000000000..d5a2151a487b --- /dev/null +++ b/crates/router/src/services/recon.rs @@ -0,0 +1,29 @@ +use error_stack::ResultExt; +use masking::Secret; + +use super::jwt; +use crate::{ + consts, + core::{self, errors::RouterResult}, + routes::app::settings::Settings, +}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ReconToken { + pub user_id: String, + pub exp: u64, +} + +impl ReconToken { + pub async fn new_token(user_id: String, settings: &Settings) -> RouterResult> { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration) + .change_context(core::errors::ApiErrorResponse::InternalServerError)? + .as_secs(); + let token_payload = Self { user_id, exp }; + let token = jwt::generate_jwt(&token_payload, settings) + .await + .change_context(core::errors::ApiErrorResponse::InternalServerError)?; + Ok(Secret::new(token)) + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 8f0b9bad3e80..0d6636e567da 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -165,6 +165,14 @@ pub enum Flow { RefundsList, // Retrieve forex flow. RetrieveForexFlow, + /// Toggles recon service for a merchant. + ReconMerchantUpdate, + /// Recon token request flow. + ReconTokenRequest, + /// Initial request for recon service. + ReconServiceRequest, + /// Recon token verification flow + ReconVerifyToken, /// Routing create flow, RoutingCreateConfig, /// Routing link config From eaa8791ee8126ebce9c500c6b426cbcfa4d3caa2 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:11:29 +0530 Subject: [PATCH 335/443] ci(s3): fetch connector creds from s3 for added security (#3323) --- .github/secrets/connector_auth.toml.gpg | Bin 3435 -> 0 bytes .../workflows/connector-ui-sanity-tests.yml | 21 ++++++++---- .../workflows/postman-collection-runner.yml | 30 ++++++++++++------ scripts/decrypt_connector_auth.sh | 10 ------ 4 files changed, 35 insertions(+), 26 deletions(-) delete mode 100644 .github/secrets/connector_auth.toml.gpg delete mode 100755 scripts/decrypt_connector_auth.sh diff --git a/.github/secrets/connector_auth.toml.gpg b/.github/secrets/connector_auth.toml.gpg deleted file mode 100644 index ce62370c1494e7173b9e45d2276ad58269e51e72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3435 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 $GITHUB_ENV + run: echo "CONNECTOR_AUTH_FILE_PATH=${HOME}/target/test/connector_auth.toml" >> $GITHUB_ENV - name: Set connector tests file path in env if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} shell: bash - run: echo "CONNECTOR_TESTS_FILE_PATH=$HOME/target/test/connector_tests.json" >> $GITHUB_ENV + run: echo "CONNECTOR_TESTS_FILE_PATH=${HOME}/target/test/connector_tests.json" >> $GITHUB_ENV - name: Set ignore_browser_profile usage in env if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }} @@ -154,9 +163,9 @@ jobs: failed_connectors=() for i in $(echo "$INPUT" | tr "," "\n"); do - echo $i + echo "${i}" if ! cargo test --package test_utils --test connectors -- "${i}_ui::" --test-threads=1; then - failed_connectors+=("$i") + failed_connectors+=("${i}") fi done diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index d5434520715f..8cbbed8187c2 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -52,27 +52,37 @@ jobs: - name: Repository checkout uses: actions/checkout@v4 - - name: Decrypt connector auth file + - name: Download Encrypted TOML from S3 and Decrypt if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} env: + AWS_ACCESS_KEY_ID: ${{ secrets.CONNECTOR_CREDS_AWS_ACCESS_KEY_ID }} + AWS_REGION: ${{ secrets.CONNECTOR_CREDS_AWS_REGION }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CONNECTOR_CREDS_AWS_SECRET_ACCESS_KEY }} CONNECTOR_AUTH_PASSPHRASE: ${{ secrets.CONNECTOR_AUTH_PASSPHRASE }} + CONNECTOR_CREDS_S3_BUCKET_URI: ${{ secrets.CONNECTOR_CREDS_S3_BUCKET_URI}} + DESTINATION_FILE_NAME: "connector_auth.toml.gpg" + S3_SOURCE_FILE_NAME: "cf05a6ab-525e-4888-98b3-3b4a443b87c0.toml.gpg" shell: bash - run: ./scripts/decrypt_connector_auth.sh + run: | + mkdir -p ${HOME}/target/secrets ${HOME}/target/test + + aws s3 cp "${CONNECTOR_CREDS_S3_BUCKET_URI}/${S3_SOURCE_FILE_NAME}" "${HOME}/target/secrets/${DESTINATION_FILE_NAME}" + gpg --quiet --batch --yes --decrypt --passphrase="${CONNECTOR_AUTH_PASSPHRASE}" --output "${HOME}/target/test/connector_auth.toml" "${HOME}/target/secrets/${DESTINATION_FILE_NAME}" - name: Set paths in env if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} id: config_path shell: bash run: | - echo "CONNECTOR_AUTH_FILE_PATH=$HOME/target/test/connector_auth.toml" >> $GITHUB_ENV + echo "CONNECTOR_AUTH_FILE_PATH=${HOME}/target/test/connector_auth.toml" >> $GITHUB_ENV - name: Fetch keys if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} env: TOML_PATH: "./config/development.toml" run: | - LOCAL_ADMIN_API_KEY=$(yq '.secrets.admin_api_key' $TOML_PATH) - echo "ADMIN_API_KEY=$LOCAL_ADMIN_API_KEY" >> $GITHUB_ENV + LOCAL_ADMIN_API_KEY=$(yq '.secrets.admin_api_key' ${TOML_PATH}) + echo "ADMIN_API_KEY=${LOCAL_ADMIN_API_KEY}" >> $GITHUB_ENV - name: Install Rust if: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} @@ -118,7 +128,7 @@ jobs: while ! nc -z localhost 8080; do if [ $COUNT -gt 12 ]; then # Wait for up to 2 minutes (12 * 10 seconds) echo "Server did not start within a reasonable time. Exiting." - kill $SERVER_PID + kill ${SERVER_PID} exit 1 else COUNT=$((COUNT+1)) @@ -141,10 +151,10 @@ jobs: export PATH=${NEWMAN_PATH}:${PATH} failed_connectors=() - for i in $(echo "$CONNECTORS" | tr "," "\n"); do - echo $i - if ! cargo run --bin test_utils -- --connector-name="$i" --base-url="$BASE_URL" --admin-api-key="$ADMIN_API_KEY"; then - failed_connectors+=("$i") + for i in $(echo "${CONNECTORS}" | tr "," "\n"); do + echo "${i}" + if ! cargo run --bin test_utils -- --connector-name="${i}" --base-url="${BASE_URL}" --admin-api-key="${ADMIN_API_KEY}"; then + failed_connectors+=("${i}") fi done diff --git a/scripts/decrypt_connector_auth.sh b/scripts/decrypt_connector_auth.sh deleted file mode 100755 index dc445f0afa6b..000000000000 --- a/scripts/decrypt_connector_auth.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env bash - -mkdir -p $HOME/target/test - - -# Decrypt the file -# --batch to prevent interactive command -# --yes to assume "yes" for questions -gpg --quiet --batch --yes --decrypt --passphrase="$CONNECTOR_AUTH_PASSPHRASE" \ ---output $HOME/target/test/connector_auth.toml .github/secrets/connector_auth.toml.gpg \ No newline at end of file From d533c98b5107fb6876c11b183eb9bc382a77a2f1 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:59:22 +0530 Subject: [PATCH 336/443] feat(connector): [BANKOFAMERICA] Implement 3DS flow for cards (#3343) --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/router/src/connector/bankofamerica.rs | 260 +++++- .../connector/bankofamerica/transformers.rs | 738 +++++++++++++++++- crates/router/src/connector/cybersource.rs | 27 + crates/router/src/core/payments.rs | 3 +- crates/router/src/core/payments/flows.rs | 2 - 8 files changed, 1017 insertions(+), 16 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index e20f9c1b65d2..d4e119641922 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -351,6 +351,7 @@ stripe = { payment_method = "bank_transfer" } nuvei = { payment_method = "card" } shift4 = { payment_method = "card" } bluesnap = { payment_method = "card" } +bankofamerica = {payment_method = "card"} cybersource = {payment_method = "card"} nmi = {payment_method = "card"} diff --git a/config/development.toml b/config/development.toml index 5732d5f0d1de..91269005a0f0 100644 --- a/config/development.toml +++ b/config/development.toml @@ -428,6 +428,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +bankofamerica = {payment_method = "card"} cybersource = {payment_method = "card"} nmi = {payment_method = "card"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c6934a64671f..450fe106a31f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -241,6 +241,7 @@ stripe = {payment_method = "bank_transfer"} nuvei = {payment_method = "card"} shift4 = {payment_method = "card"} bluesnap = {payment_method = "card"} +bankofamerica = {payment_method = "card"} cybersource = {payment_method = "card"} nmi = {payment_method = "card"} diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index aeb3dafcfa21..0d901b990784 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -12,6 +12,7 @@ use time::OffsetDateTime; use transformers as bankofamerica; use url::Url; +use super::utils::{PaymentsAuthorizeRequestData, RouterData}; use crate::{ configs::settings, connector::{utils as connector_utils, utils::RefundsRequestData}, @@ -48,6 +49,8 @@ impl api::Refund for Bankofamerica {} impl api::RefundExecute for Bankofamerica {} impl api::RefundSync for Bankofamerica {} impl api::PaymentToken for Bankofamerica {} +impl api::PaymentsPreProcessing for Bankofamerica {} +impl api::PaymentsCompleteAuthorize for Bankofamerica {} impl Bankofamerica { pub fn generate_digest(&self, payload: &[u8]) -> String { @@ -299,6 +302,113 @@ impl } } +impl + ConnectorIntegration< + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + 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::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let redirect_response = req.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => Ok(format!( + "{}risk/v1/authentications", + self.base_url(connectors) + )), + Some(_) | None => Ok(format!( + "{}risk/v1/authentication-results", + self.base_url(connectors) + )), + } + } + fn get_request_body( + &self, + req: &types::PaymentsPreProcessingRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + req, + ))?; + let connector_req = + bankofamerica::BankOfAmericaPreProcessingRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsPreProcessingType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaPreProcessingResponse = res + .response + .parse_struct("BankOfAmerica AuthEnrollmentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Bankofamerica { @@ -316,13 +426,17 @@ impl ConnectorIntegration CustomResult { - Ok(format!( - "{}pts/v2/payments/", - api::ConnectorCommon::base_url(self, connectors) - )) + if req.is_three_ds() && req.request.is_card() { + Ok(format!( + "{}risk/v1/authentication-setups", + self.base_url(connectors) + )) + } else { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } } fn get_request_body( @@ -336,9 +450,15 @@ impl ConnectorIntegration CustomResult { + if data.is_three_ds() && data.request.is_card() { + let response: bankofamerica::BankOfAmericaAuthSetupResponse = res + .response + .parse_struct("Bankofamerica AuthSetupResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } else { + let response: bankofamerica::BankOfAmericaPaymentsResponse = res + .response + .parse_struct("Bankofamerica PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } + + fn get_5xx_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: bankofamerica::BankOfAmericaServerErrorResponse = res + .response + .parse_struct("BankOfAmericaServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } +} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Bankofamerica +{ + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + 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::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}pts/v2/payments/", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = bankofamerica::BankOfAmericaRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_req = + bankofamerica::BankOfAmericaPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: Response, + ) -> CustomResult { let response: bankofamerica::BankOfAmericaPaymentsResponse = res .response .parse_struct("BankOfAmerica PaymentResponse") diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 6abe1b634df6..72e3de0bf777 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -1,6 +1,7 @@ use api_models::payments; use base64::Engine; -use common_utils::pii; +use common_utils::{ext_traits::ValueExt, pii}; +use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -8,10 +9,12 @@ use serde_json::Value; use crate::{ connector::utils::{ self, AddressDetailsData, ApplePayDecrypt, CardData, CardIssuer, - PaymentsAuthorizeRequestData, PaymentsSyncRequestData, RouterData, + PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, + PaymentsPreProcessingData, PaymentsSyncRequestData, RouterData, }, consts, core::errors, + services, types::{ self, api::{self, enums as api_enums}, @@ -85,14 +88,17 @@ pub struct BankOfAmericaPaymentsRequest { order_information: OrderInformationWithBill, client_reference_information: ClientReferenceInformation, #[serde(skip_serializing_if = "Option::is_none")] + consumer_authentication_information: Option, + #[serde(skip_serializing_if = "Option::is_none")] merchant_defined_information: Option>, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProcessingInformation { - capture: bool, + capture: Option, payment_solution: Option, + commerce_indicator: String, } #[derive(Debug, Serialize)] @@ -102,6 +108,17 @@ pub struct MerchantDefinedInformation { value: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformation { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + directory_server_transaction_id: Option, + specification_version: Option, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CaptureOptions { @@ -287,6 +304,28 @@ impl } } +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + BillTo, + )> for OrderInformationWithBill +{ + fn from( + (item, bill_to): ( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + BillTo, + ), + ) -> Self { + Self { + amount_details: Amount { + total_amount: item.amount.to_owned(), + currency: item.router_data.request.currency, + }, + bill_to, + } + } +} + impl From<( &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, @@ -300,11 +339,40 @@ impl ), ) -> Self { Self { - capture: matches!( + capture: Some(matches!( item.router_data.request.capture_method, Some(enums::CaptureMethod::Automatic) | None - ), + )), payment_solution: solution.map(String::from), + commerce_indicator: String::from("internet"), + } + } +} + +impl + From<( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &BankOfAmericaConsumerAuthValidateResponse, + )> for ProcessingInformation +{ + fn from( + (item, solution, three_ds_data): ( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + Option, + &BankOfAmericaConsumerAuthValidateResponse, + ), + ) -> Self { + Self { + capture: Some(matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Automatic) | None + )), + payment_solution: solution.map(String::from), + commerce_indicator: three_ds_data + .indicator + .to_owned() + .unwrap_or(String::from("internet")), } } } @@ -319,6 +387,16 @@ impl From<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> } } +impl From<&BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for ClientReferenceInformation +{ + fn from(item: &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>) -> Self { + Self { + code: Some(item.router_data.connector_request_reference_id.clone()), + } + } +} + impl ForeignFrom for Vec { fn foreign_from(metadata: Value) -> Self { let hashmap: std::collections::BTreeMap = @@ -367,6 +445,83 @@ pub struct Avs { code_raw: String, } +impl + TryFrom<( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + )> for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, ccard): ( + &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + payments::Card, + ), + ) -> Result { + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + let client_reference_information = ClientReferenceInformation::from(item); + + let three_ds_info: BankOfAmericaThreeDSMetadata = item + .router_data + .request + .connector_meta + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "connector_meta", + })? + .parse_value("BankOfAmericaThreeDSMetadata") + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; + + let processing_information = + ProcessingInformation::from((item, None, &three_ds_info.three_ds_data)); + + let consumer_authentication_information = Some(BankOfAmericaConsumerAuthInformation { + ucaf_collection_indicator: three_ds_info.three_ds_data.ucaf_collection_indicator, + cavv: three_ds_info.three_ds_data.cavv, + ucaf_authentication_data: three_ds_info.three_ds_data.ucaf_authentication_data, + xid: three_ds_info.three_ds_data.xid, + directory_server_transaction_id: three_ds_info + .three_ds_data + .directory_server_transaction_id, + specification_version: three_ds_info.three_ds_data.specification_version, + }); + + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information, + merchant_defined_information, + }) + } +} + impl TryFrom<( &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, @@ -410,6 +565,7 @@ impl order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } } @@ -455,6 +611,7 @@ impl order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } } @@ -496,6 +653,7 @@ impl order_information, client_reference_information, merchant_defined_information, + consumer_authentication_information: None, }) } } @@ -552,6 +710,7 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> order_information, merchant_defined_information, client_reference_information, + consumer_authentication_information: None, }) } } @@ -608,6 +767,64 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> } } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaAuthSetupRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> + for BankOfAmericaAuthSetupRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + let payment_information = PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + }); + let client_reference_information = ClientReferenceInformation::from(item); + Ok(Self { + payment_information, + client_reference_information, + }) + } + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("BankOfAmerica"), + ) + .into()) + } + } + } +} + #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BankofamericaPaymentStatus { @@ -669,6 +886,29 @@ impl ForeignFrom<(BankofamericaPaymentStatus, bool)> for enums::AttemptStatus { } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationResponse { + access_token: String, + device_data_collection_url: String, + reference_id: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthSetupInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaAuthSetupResponse { + ClientAuthSetupInfo(ClientAuthSetupInfoResponse), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum BankOfAmericaPaymentsResponse { @@ -799,6 +1039,494 @@ fn get_payment_response( } } +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaAuthSetupResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaAuthSetupResponse::ClientAuthSetupInfo(info_response) => Ok(Self { + status: enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::CybersourceAuthSetup { + access_token: info_response + .consumer_authentication_information + .access_token, + ddc_url: info_response + .consumer_authentication_information + .device_data_collection_url, + reference_id: info_response + .consumer_authentication_information + .reference_id, + }), + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ), + incremental_authorization_allowed: None, + }), + ..item.data + }), + BankOfAmericaAuthSetupResponse::ErrorInformation(error_response) => { + let error_reason = error_response + .error_information + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason; + Ok(Self { + response: Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }), + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationRequest { + return_url: String, + reference_id: String, +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaAuthEnrollmentRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationRequest, + order_information: OrderInformationWithBill, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct BankOfAmericaRedirectionAuthResponse { + pub transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationValidateRequest { + authentication_transaction_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaAuthValidateRequest { + payment_information: PaymentInformation, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationValidateRequest, + order_information: OrderInformation, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum BankOfAmericaPreProcessingRequest { + AuthEnrollment(BankOfAmericaAuthEnrollmentRequest), + AuthValidate(BankOfAmericaAuthValidateRequest), +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsPreProcessingRouterData>> + for BankOfAmericaPreProcessingRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsPreProcessingRouterData>, + ) -> Result { + let client_reference_information = ClientReferenceInformation { + code: Some(item.router_data.connector_request_reference_id.clone()), + }; + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "payment_method_data", + }, + )?; + let payment_information = match payment_method_data { + payments::PaymentMethodData::Card(ccard) => { + let card_issuer = ccard.get_card_issuer(); + let card_type = match card_issuer { + Ok(issuer) => Some(String::from(issuer)), + Err(_) => None, + }; + Ok(PaymentInformation::Cards(CardPaymentInformation { + card: Card { + number: ccard.card_number, + expiration_month: ccard.card_exp_month, + expiration_year: ccard.card_exp_year, + security_code: ccard.card_cvc, + card_type, + }, + })) + } + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("BankOfAmerica"), + )) + } + }?; + + let redirect_response = item.router_data.request.redirect_response.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "redirect_response", + }, + )?; + + let amount_details = Amount { + total_amount: item.amount.clone(), + currency: item.router_data.request.currency.ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "currency", + }, + )?, + }; + + match redirect_response.params { + Some(param) if !param.clone().peek().is_empty() => { + let reference_id = param + .clone() + .peek() + .split_once('=') + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.params.reference_id", + })? + .1 + .to_string(); + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill { + amount_details, + bill_to, + }; + Ok(Self::AuthEnrollment(BankOfAmericaAuthEnrollmentRequest { + payment_information, + client_reference_information, + consumer_authentication_information: + BankOfAmericaConsumerAuthInformationRequest { + return_url: item.router_data.request.get_complete_authorize_url()?, + reference_id, + }, + order_information, + })) + } + Some(_) | None => { + let redirect_payload: BankOfAmericaRedirectionAuthResponse = redirect_response + .payload + .ok_or(errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "request.redirect_response.payload", + })? + .peek() + .clone() + .parse_value("BankOfAmericaRedirectionAuthResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let order_information = OrderInformation { amount_details }; + Ok(Self::AuthValidate(BankOfAmericaAuthValidateRequest { + payment_information, + client_reference_information, + consumer_authentication_information: + BankOfAmericaConsumerAuthInformationValidateRequest { + authentication_transaction_id: redirect_payload.transaction_id, + }, + order_information, + })) + } + } + } +} + +impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>> + for BankOfAmericaPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &BankOfAmericaRouterData<&types::PaymentsCompleteAuthorizeRouterData>, + ) -> Result { + let payment_method_data = item.router_data.request.payment_method_data.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "payment_method_data", + }, + )?; + match payment_method_data { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("BankOfAmerica"), + ) + .into()) + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum BankOfAmericaAuthEnrollmentStatus { + PendingAuthentication, + AuthenticationSuccessful, + AuthenticationFailed, +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthValidateResponse { + ucaf_collection_indicator: Option, + cavv: Option, + ucaf_authentication_data: Option, + xid: Option, + specification_version: Option, + directory_server_transaction_id: Option, + indicator: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BankOfAmericaThreeDSMetadata { + three_ds_data: BankOfAmericaConsumerAuthValidateResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOfAmericaConsumerAuthInformationEnrollmentResponse { + access_token: Option, + step_up_url: Option, + //Added to segregate the three_ds_data in a separate struct + #[serde(flatten)] + validate_response: BankOfAmericaConsumerAuthValidateResponse, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientAuthCheckInfoResponse { + id: String, + client_reference_information: ClientReferenceInformation, + consumer_authentication_information: BankOfAmericaConsumerAuthInformationEnrollmentResponse, + status: BankOfAmericaAuthEnrollmentStatus, + error_information: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum BankOfAmericaPreProcessingResponse { + ClientAuthCheckInfo(Box), + ErrorInformation(BankOfAmericaErrorInformationResponse), +} + +impl From for enums::AttemptStatus { + fn from(item: BankOfAmericaAuthEnrollmentStatus) -> Self { + match item { + BankOfAmericaAuthEnrollmentStatus::PendingAuthentication => Self::AuthenticationPending, + BankOfAmericaAuthEnrollmentStatus::AuthenticationSuccessful => { + Self::AuthenticationSuccessful + } + BankOfAmericaAuthEnrollmentStatus::AuthenticationFailed => Self::AuthenticationFailed, + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPreProcessingResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPreProcessingResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPreProcessingResponse::ClientAuthCheckInfo(info_response) => { + let status = enums::AttemptStatus::from(info_response.status); + let risk_info: Option = None; + if utils::is_payment_failure(status) { + let response = Err(types::ErrorResponse::from(( + &info_response.error_information, + &risk_info, + item.http_code, + info_response.id.clone(), + ))); + + Ok(Self { + status, + response, + ..item.data + }) + } else { + let connector_response_reference_id = Some( + info_response + .client_reference_information + .code + .unwrap_or(info_response.id.clone()), + ); + + let redirection_data = match ( + info_response + .consumer_authentication_information + .access_token, + info_response + .consumer_authentication_information + .step_up_url, + ) { + (Some(access_token), Some(step_up_url)) => { + Some(services::RedirectForm::CybersourceConsumerAuth { + access_token, + step_up_url, + }) + } + _ => None, + }; + let three_ds_data = serde_json::to_value( + info_response + .consumer_authentication_information + .validate_response, + ) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data, + mandate_reference: None, + connector_metadata: Some( + serde_json::json!({"three_ds_data":three_ds_data}), + ), + network_txn_id: None, + connector_response_reference_id, + incremental_authorization_allowed: None, + }), + ..item.data + }) + } + } + BankOfAmericaPreProcessingResponse::ErrorInformation(ref error_response) => { + let error_reason = error_response + .error_information + .message + .to_owned() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()); + let error_message = error_response.error_information.reason.to_owned(); + let response = Err(types::ErrorResponse { + code: error_message + .clone() + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(error_reason), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(error_response.id.clone()), + }); + Ok(Self { + response, + status: enums::AttemptStatus::AuthenticationFailed, + ..item.data + }) + } + } + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + BankOfAmericaPaymentsResponse, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + match item.response { + BankOfAmericaPaymentsResponse::ClientReferenceInformation(info_response) => { + let status = enums::AttemptStatus::foreign_from(( + info_response.status.clone(), + item.data.request.is_auto_capture()?, + )); + let response = get_payment_response((&info_response, status, item.http_code)); + Ok(Self { + status, + response, + ..item.data + }) + } + BankOfAmericaPaymentsResponse::ErrorInformation(ref error_response) => { + Ok(Self::from(( + &error_response.clone(), + item, + Some(enums::AttemptStatus::Failure), + ))) + } + } + } +} + impl TryFrom< types::ResponseRouterData< diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 69159c10c8af..b300e97b44a9 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -874,6 +874,33 @@ impl ConnectorIntegration CustomResult { self.build_error_response(res) } + + fn get_5xx_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: cybersource::CybersourceServerErrorResponse = res + .response + .parse_struct("CybersourceServerErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let attempt_status = match response.reason { + Some(reason) => match reason { + transformers::Reason::SystemError => Some(enums::AttemptStatus::Failure), + transformers::Reason::ServerTimeout | transformers::Reason::ServiceTimeout => None, + }, + None => None, + }; + Ok(types::ErrorResponse { + status_code: res.status_code, + reason: response.status.clone(), + code: response.status.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .message + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + attempt_status, + connector_transaction_id: None, + }) + } } impl diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 21cdec92ccb4..49a9bcf66645 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1489,7 +1489,8 @@ where router_data = router_data.preprocessing_steps(state, connector).await?; (router_data, false) - } else if connector.connector_name == router_types::Connector::Cybersource + } else if (connector.connector_name == router_types::Connector::Cybersource + || connector.connector_name == router_types::Connector::Bankofamerica) && is_operation_complete_authorize(&operation) && router_data.auth_type == storage_enums::AuthenticationType::ThreeDs { diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 6dd692f15259..c9f9d6d87f5c 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -147,7 +147,6 @@ impl default_imp_for_complete_authorize!( connector::Aci, connector::Adyen, - connector::Bankofamerica, connector::Bitpay, connector::Boku, connector::Cashtocode, @@ -863,7 +862,6 @@ default_imp_for_pre_processing_steps!( connector::Airwallex, connector::Authorizedotnet, connector::Bambora, - connector::Bankofamerica, connector::Bitpay, connector::Bluesnap, connector::Boku, From 398c5ed51e0547504c3dfbd1d7c23568337e7d1c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 00:20:08 +0000 Subject: [PATCH 337/443] chore(version): 2024.01.17.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 739b8cd2c667..b9b01fe4e915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.17.0 + +### Features + +- **connector:** [BANKOFAMERICA] Implement 3DS flow for cards ([#3343](https://github.com/juspay/hyperswitch/pull/3343)) ([`d533c98`](https://github.com/juspay/hyperswitch/commit/d533c98b5107fb6876c11b183eb9bc382a77a2f1)) +- **recon:** Add recon APIs ([#3345](https://github.com/juspay/hyperswitch/pull/3345)) ([`8678f8d`](https://github.com/juspay/hyperswitch/commit/8678f8d1448b5ce430931bfbbc269ef979d9eea7)) + +### Bug Fixes + +- **connector_onboarding:** Check if connector exists for the merchant account and add reset tracking id API ([#3229](https://github.com/juspay/hyperswitch/pull/3229)) ([`58cc8d6`](https://github.com/juspay/hyperswitch/commit/58cc8d6109ce49d385b06c762ab3f6670f5094eb)) +- **payment_link:** Added expires_on in payment response ([#3332](https://github.com/juspay/hyperswitch/pull/3332)) ([`5ad3f89`](https://github.com/juspay/hyperswitch/commit/5ad3f8939afafce3eec39704dcaa92270b384dcd)) + +**Full Changelog:** [`2024.01.12.1...2024.01.17.0`](https://github.com/juspay/hyperswitch/compare/2024.01.12.1...2024.01.17.0) + +- - - + ## 2024.01.12.1 ### Miscellaneous Tasks From 01c2de223f60595d77c06a59a40dfe041e02cfee Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:35:13 +0530 Subject: [PATCH 338/443] feat(payment_method): add capability to store bank details using /payment_methods endpoint (#3113) Co-authored-by: Kashif Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 15 ++ crates/api_models/src/payouts.rs | 38 ++-- .../src/connector/adyen/transformers.rs | 85 +++++---- crates/router/src/core/locker_migration.rs | 1 + .../router/src/core/payment_methods/cards.rs | 175 ++++++++++++++---- .../src/core/payment_methods/transformers.rs | 24 +++ .../router/src/core/payment_methods/vault.rs | 13 +- crates/router/src/core/payments/helpers.rs | 2 + .../router/src/core/payments/tokenization.rs | 1 + crates/router/src/core/payouts/helpers.rs | 118 ++++++++---- .../router/src/types/api/payment_methods.rs | 27 ++- crates/router/tests/connectors/adyen.rs | 8 +- crates/router/tests/connectors/wise.rs | 6 +- openapi/openapi_spec.json | 72 +++++-- 14 files changed, 412 insertions(+), 173 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index a907fff60193..3467777da745 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -55,6 +55,11 @@ pub struct PaymentMethodCreate { /// The card network #[schema(example = "Visa")] pub card_network: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -72,6 +77,11 @@ pub struct PaymentMethodUpdate { #[schema(value_type = Option,example = "Visa")] pub card_network: Option, + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, @@ -147,6 +157,11 @@ pub struct PaymentMethodResponse { #[schema(value_type = Option, example = "2023-01-18T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, + + /// Payment method details from locker + #[cfg(feature = "payouts")] + #[schema(value_type = Option)] + pub bank_transfer: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index f7dba2446e91..9e771b471214 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -181,7 +181,7 @@ pub struct Card { /// The card holder's name #[schema(value_type = String, example = "John Doe")] - pub card_holder_name: Secret, + pub card_holder_name: Option>, } #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] @@ -195,16 +195,16 @@ pub enum Bank { #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct AchBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// Bank account number is an unique identifier assigned by a bank to a customer. #[schema(value_type = String, example = "000123456")] @@ -218,16 +218,16 @@ pub struct AchBankTransfer { #[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct BacsBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// Bank account number is an unique identifier assigned by a bank to a customer. #[schema(value_type = String, example = "000123456")] @@ -242,16 +242,16 @@ pub struct BacsBankTransfer { // The SEPA (Single Euro Payments Area) is a pan-European network that allows you to send and receive payments in euros between two cross-border bank accounts in the eurozone. pub struct SepaBankTransfer { /// Bank name - #[schema(value_type = String, example = "Deutsche Bank")] - pub bank_name: String, + #[schema(value_type = Option, example = "Deutsche Bank")] + pub bank_name: Option, /// Bank country code - #[schema(value_type = CountryAlpha2, example = "US")] - pub bank_country_code: api_enums::CountryAlpha2, + #[schema(value_type = Option, example = "US")] + pub bank_country_code: Option, /// Bank city - #[schema(value_type = String, example = "California")] - pub bank_city: String, + #[schema(value_type = Option, example = "California")] + pub bank_city: Option, /// International Bank Account Number (iban) - used in many countries for identifying a bank along with it's customer. #[schema(value_type = String, example = "DE89370400440532013000")] diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index e00b829f2834..1e1cfa8fe50c 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -9,9 +9,7 @@ use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime, PrimitiveDateTime}; #[cfg(feature = "payouts")] -use crate::connector::utils::AddressDetailsData; -#[cfg(feature = "payouts")] -use crate::types::api::payouts; +use crate::{connector::utils::AddressDetailsData, types::api::payouts, utils::OptionExt}; use crate::{ connector::utils::{ self, BrowserInformationData, CardData, MandateReferenceData, PaymentsAuthorizeRequestData, @@ -1707,20 +1705,6 @@ fn get_country_code( address.and_then(|billing| billing.address.as_ref().and_then(|address| address.country)) } -#[cfg(feature = "payouts")] -fn get_payout_card_details(payout_method_data: &PayoutMethodData) -> Option { - match payout_method_data { - PayoutMethodData::Card(card) => Some(PayoutCardDetails { - _type: "scheme".to_string(), // FIXME: Remove hardcoding - number: card.card_number.peek().to_string(), - expiry_month: card.expiry_month.peek().to_string(), - expiry_year: card.expiry_year.peek().to_string(), - holder_name: card.card_holder_name.peek().to_string(), - }), - _ => None, - } -} - fn get_social_security_number( voucher_data: &api_models::payments::VoucherData, ) -> Option> { @@ -3980,12 +3964,12 @@ pub struct AdyenPayoutCreateRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct PayoutBankDetails { - bank_name: String, + iban: Secret, + owner_name: Secret, + bank_city: Option, + bank_name: Option, bic: Option>, - country_code: storage_enums::CountryAlpha2, - iban: Option>, - owner_name: Option>, - bank_city: String, + country_code: Option, tax_id: Option>, } @@ -4036,11 +4020,11 @@ pub struct AdyenPayoutEligibilityRequest { #[serde(rename_all = "camelCase")] pub struct PayoutCardDetails { #[serde(rename = "type")] - _type: String, - number: String, - expiry_month: String, - expiry_year: String, - holder_name: String, + payment_method_type: String, + number: CardNumber, + expiry_month: Secret, + expiry_year: Secret, + holder_name: Secret, } #[cfg(feature = "payouts")] @@ -4095,6 +4079,31 @@ pub struct AdyenPayoutCancelRequest { merchant_account: Secret, } +#[cfg(feature = "payouts")] +impl TryFrom<&PayoutMethodData> for PayoutCardDetails { + type Error = Error; + fn try_from(item: &PayoutMethodData) -> Result { + match item { + PayoutMethodData::Card(card) => Ok(Self { + payment_method_type: "scheme".to_string(), // FIXME: Remove hardcoding + number: card.card_number.clone(), + expiry_month: card.expiry_month.clone(), + expiry_year: card.expiry_year.clone(), + holder_name: card + .card_holder_name + .clone() + .get_required_value("card_holder_name") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card.holder_name", + })?, + }), + _ => Err(errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.card", + })?, + } + } +} + // Payouts eligibility request transform #[cfg(feature = "payouts")] impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutEligibilityRequest { @@ -4102,12 +4111,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE fn try_from(item: &AdyenRouterData<&types::PayoutsRouterData>) -> Result { let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; let payout_method_data = - get_payout_card_details(&item.router_data.get_payout_method_data()?).map_or( - Err(errors::ConnectorError::MissingRequiredField { - field_name: "payout_method_data", - }), - Ok, - )?; + PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?; Ok(Self { amount: Amount { currency: item.router_data.request.destination_currency, @@ -4155,6 +4159,11 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC .customer_details .to_owned() .map_or((None, None), |c| (c.name, c.email)); + let owner_name = owner_name.get_required_value("owner_name").change_context( + errors::ConnectorError::MissingRequiredField { + field_name: "payout_method_data.bank.owner_name", + }, + )?; match item.router_data.get_payout_method_data()? { PayoutMethodData::Card(_) => Err(errors::ConnectorError::NotSupported { @@ -4169,7 +4178,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC bank_city: b.bank_city, owner_name, bic: b.bic, - iban: Some(b.iban), + iban: b.iban, tax_id: None, }, payouts::BankPayout::Ach(..) => Err(errors::ConnectorError::NotSupported { @@ -4234,13 +4243,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF value: item.amount.to_owned(), currency: item.router_data.request.destination_currency, }, - card: get_payout_card_details(&item.router_data.get_payout_method_data()?) - .map_or( - Err(errors::ConnectorError::MissingRequiredField { - field_name: "payout_method_data", - }), - Ok, - )?, + card: PayoutCardDetails::try_from(&item.router_data.get_payout_method_data()?)?, billing_address: get_address_info(item.router_data.get_billing().ok()), merchant_account, reference: item.router_data.request.payout_id.clone(), diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index 3f56cddee126..e3e308a8a01c 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -109,6 +109,7 @@ pub async fn call_to_locker( payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, card: Some(card_details.clone()), + bank_transfer: None, metadata: pm.metadata, customer_id: Some(pm.customer_id), card_network: card.card_brand, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 39bc54fa1578..51f543536350 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -105,6 +105,29 @@ pub async fn create_payment_method( Ok(response) } +pub fn store_default_payment_method( + req: &api::PaymentMethodCreate, + customer_id: &str, + merchant_id: &String, +) -> (api::PaymentMethodResponse, bool) { + let pm_id = generate_id(consts::ID_LENGTH, "pm"); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id.to_owned()), + payment_method_id: pm_id, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + bank_transfer: None, + card: None, + metadata: req.metadata.clone(), + created: Some(common_utils::date_time::now()), + recurring_enabled: false, //[#219] + installment_payment_enabled: false, //[#219] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + }; + (payment_method_response, false) +} + #[instrument(skip_all)] pub async fn add_payment_method( state: routes::AppState, @@ -115,30 +138,44 @@ pub async fn add_payment_method( req.validate()?; let merchant_id = &merchant_account.merchant_id; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; - let response = match req.card.clone() { - Some(card) => { - add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Add Card Failed") - } - None => { - let pm_id = generate_id(consts::ID_LENGTH, "pm"); - let payment_method_response = api::PaymentMethodResponse { - merchant_id: merchant_id.to_string(), - customer_id: Some(customer_id.clone()), - payment_method_id: pm_id, - payment_method: req.payment_method, - payment_method_type: req.payment_method_type, - card: None, - metadata: req.metadata.clone(), - created: Some(common_utils::date_time::now()), - recurring_enabled: false, //[#219] - installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] - }; - Ok((payment_method_response, false)) - } + + let response = match req.payment_method { + api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() { + Some(bank) => add_bank_to_locker( + &state, + req.clone(), + merchant_account, + key_store, + &bank, + &customer_id, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add PaymentMethod Failed"), + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + api_enums::PaymentMethod::Card => match req.card.clone() { + Some(card) => { + add_card_to_locker(&state, req.clone(), &card, &customer_id, merchant_account) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Add Card Failed") + } + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), + }, + _ => Ok(store_default_payment_method( + &req, + &customer_id, + merchant_id, + )), }; let (resp, is_duplicate) = response?; @@ -199,6 +236,7 @@ pub async fn update_customer_payment_method( payment_method_type: pm.payment_method_type, payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, + bank_transfer: req.bank_transfer, card: req.card, metadata: req.metadata, customer_id: Some(pm.customer_id), @@ -212,6 +250,64 @@ pub async fn update_customer_payment_method( // Wrapper function to switch lockers +pub async fn add_bank_to_locker( + state: &routes::AppState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + bank: &api::BankPayout, + customer_id: &String, +) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> { + let key = key_store.key.get_inner().peek(); + let payout_method_data = api::PayoutMethodData::Bank(bank.clone()); + let enc_data = async { + serde_json::to_value(payout_method_data.to_owned()) + .map_err(|err| { + logger::error!("Error while encoding payout method data: {}", err); + errors::VaultError::SavePaymentMethodFailed + }) + .into_report() + .change_context(errors::VaultError::SavePaymentMethodFailed) + .attach_printable("Unable to encode payout method data") + .ok() + .map(|v| { + let secret: Secret = Secret::new(v.to_string()); + secret + }) + .async_lift(|inner| encrypt_optional(inner, key)) + .await + } + .await + .change_context(errors::VaultError::SavePaymentMethodFailed) + .attach_printable("Failed to encrypt payout method data")? + .map(Encryption::from) + .map(|e| e.into_inner()) + .map_or(Err(errors::VaultError::SavePaymentMethodFailed), |e| { + Ok(hex::encode(e.peek())) + })?; + + let payload = + payment_methods::StoreLockerReq::LockerGeneric(payment_methods::StoreGenericReq { + merchant_id: &merchant_account.merchant_id, + merchant_customer_id: customer_id.to_owned(), + enc_data, + }); + let store_resp = call_to_locker_hs( + state, + &payload, + customer_id, + api_enums::LockerChoice::Basilisk, + ) + .await?; + let payment_method_resp = payment_methods::mk_add_bank_response_hs( + bank.clone(), + store_resp.card_reference, + req, + &merchant_account.merchant_id, + ); + Ok((payment_method_resp, store_resp.duplicate.unwrap_or(false))) +} + /// The response will be the tuple of PaymentMethodResponse and the duplication check of payment_method pub async fn add_card_to_locker( state: &routes::AppState, @@ -2424,7 +2520,17 @@ pub async fn list_customer_payment_method( let token_data = PaymentTokenData::temporary_generic(token.clone()); ( None, - Some(get_lookup_key_for_payout_method(state, &key_store, &token, &pm).await?), + Some( + get_bank_from_hs_locker( + state, + &key_store, + &token, + &pm.customer_id, + &pm.customer_id, + &pm.payment_method_id, + ) + .await?, + ), token_data, ) } @@ -2738,18 +2844,20 @@ async fn get_bank_account_connector_details( } #[cfg(feature = "payouts")] -pub async fn get_lookup_key_for_payout_method( +pub async fn get_bank_from_hs_locker( state: &routes::AppState, key_store: &domain::MerchantKeyStore, - payout_token: &str, - pm: &storage::PaymentMethod, + temp_token: &str, + customer_id: &str, + merchant_id: &str, + token_ref: &str, ) -> errors::RouterResult { let payment_method = get_payment_method_from_hs_locker( state, key_store, - &pm.customer_id, - &pm.merchant_id, - &pm.payment_method_id, + customer_id, + merchant_id, + token_ref, None, ) .await @@ -2764,9 +2872,9 @@ pub async fn get_lookup_key_for_payout_method( api::PayoutMethodData::Bank(bank) => { vault::Vault::store_payout_method_data_in_locker( state, - Some(payout_token.to_string()), + Some(temp_token.to_string()), &pm_parsed, - Some(pm.customer_id.to_owned()), + Some(customer_id.to_owned()), key_store, ) .await @@ -2893,6 +3001,7 @@ pub async fn retrieve_payment_method( payment_method_id: pm.payment_method_id, payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, + bank_transfer: None, card, metadata: pm.metadata, created: Some(pm.created_at), diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 5506dc7eb9ac..da4f03b49c1e 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -324,6 +324,28 @@ pub async fn mk_add_locker_request_hs<'a>( Ok(request) } +pub fn mk_add_bank_response_hs( + bank: api::BankPayout, + bank_reference: String, + req: api::PaymentMethodCreate, + merchant_id: &str, +) -> api::PaymentMethodResponse { + api::PaymentMethodResponse { + merchant_id: merchant_id.to_owned(), + customer_id: req.customer_id, + payment_method_id: bank_reference, + payment_method: req.payment_method, + payment_method_type: req.payment_method_type, + bank_transfer: Some(bank), + card: None, + metadata: req.metadata, + created: Some(common_utils::date_time::now()), + recurring_enabled: false, // [#256] + installment_payment_enabled: false, // #[#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + } +} + pub fn mk_add_card_response_hs( card: api::CardDetail, card_reference: String, @@ -349,6 +371,7 @@ pub fn mk_add_card_response_hs( payment_method_id: card_reference, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + bank_transfer: None, card: Some(card), metadata: req.metadata, created: Some(common_utils::date_time::now()), @@ -383,6 +406,7 @@ pub fn mk_add_card_response( payment_method_id: response.card_id, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + bank_transfer: None, card: Some(card), metadata: req.metadata, created: Some(common_utils::date_time::now()), diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 070bca234c8e..063b69687577 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -351,7 +351,7 @@ impl Vaultable for api::CardPayout { card_number: self.card_number.peek().clone(), exp_year: self.expiry_year.peek().clone(), exp_month: self.expiry_month.peek().clone(), - name_on_card: Some(self.card_holder_name.peek().clone()), + name_on_card: self.card_holder_name.clone().map(|n| n.peek().to_string()), nickname: None, card_last_four: None, card_token: None, @@ -397,7 +397,7 @@ impl Vaultable for api::CardPayout { .map_err(|_| errors::VaultError::FetchCardFailed)?, expiry_month: value1.exp_month.into(), expiry_year: value1.exp_year.into(), - card_holder_name: value1.name_on_card.unwrap_or_default().into(), + card_holder_name: value1.name_on_card.map(masking::Secret::new), }; let supp_data = SupplementaryVaultData { @@ -421,9 +421,9 @@ pub struct TokenizedBankSensitiveValues { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenizedBankInsensitiveValues { pub customer_id: Option, - pub bank_name: String, - pub bank_country_code: api::enums::CountryAlpha2, - pub bank_city: String, + pub bank_name: Option, + pub bank_country_code: Option, + pub bank_city: Option, } #[cfg(feature = "payouts")] @@ -702,7 +702,8 @@ impl Vault { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error getting Value2 for locker")?; - let lookup_key = token_id.unwrap_or_else(|| generate_id_with_default_len("token")); + let lookup_key = + token_id.unwrap_or_else(|| generate_id_with_default_len("temporary_token")); let lookup_key = create_tokenize( state, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 7230d74e9a98..92dc1bf5f4b3 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1041,6 +1041,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: card.card_issuer.clone(), payment_method_issuer_code: None, + bank_transfer: None, card: Some(card_detail), metadata: None, customer_id: Some(customer_id), @@ -1057,6 +1058,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: None, payment_method_issuer_code: None, + bank_transfer: None, card: None, metadata: None, customer_id: Some(customer.customer_id.to_owned()), diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 551d1c8abb9a..f884cb79e7e1 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -198,6 +198,7 @@ pub async fn save_in_locker( payment_method_id: pm_id, payment_method: payment_method_request.payment_method, payment_method_type: payment_method_request.payment_method_type, + bank_transfer: None, card: None, metadata: None, created: Some(common_utils::date_time::now()), diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 9ddc8395738e..56e3a6faf537 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,4 +1,7 @@ -use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use common_utils::{ + errors::CustomResult, + ext_traits::{StringExt, ValueExt}, +}; use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; @@ -40,28 +43,50 @@ pub async fn make_payout_method_data<'a>( merchant_key_store: &domain::MerchantKeyStore, ) -> RouterResult> { let db = &*state.store; + let certain_payout_type = payout_type.get_required_value("payout_type")?.to_owned(); let hyperswitch_token = if let Some(payout_token) = payout_token { - let key = format!( - "pm_token_{}_{}_hyperswitch", - payout_token, - api_enums::PaymentMethod::foreign_from( - payout_type.get_required_value("payout_type")?.to_owned() - ) - ); - - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - - let hyperswitch_token_option = redis_conn - .get_key::>(&key) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch the token from redis")?; + if payout_token.starts_with("temporary_token_") { + Some(payout_token.to_string()) + } else { + let key = format!( + "pm_token_{}_{}_hyperswitch", + payout_token, + api_enums::PaymentMethod::foreign_from(certain_payout_type) + ); + + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; - hyperswitch_token_option.or(Some(payout_token.to_string())) + let hyperswitch_token = redis_conn + .get_key::>(&key) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch the token from redis")? + .ok_or(error_stack::Report::new( + errors::ApiErrorResponse::UnprocessableEntity { + message: "Token is invalid or expired".to_owned(), + }, + ))?; + let payment_token_data = hyperswitch_token + .clone() + .parse_struct("PaymentTokenData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to deserialize hyperswitch token data")?; + + let payment_token = match payment_token_data { + storage::PaymentTokenData::PermanentCard(storage::CardTokenData { token }) => { + Some(token) + } + storage::PaymentTokenData::TemporaryGeneric(storage::GenericTokenData { + token, + }) => Some(token), + _ => None, + }; + payment_token.or(Some(payout_token.to_string())) + } } else { None }; @@ -69,8 +94,10 @@ pub async fn make_payout_method_data<'a>( match (payout_method_data.to_owned(), hyperswitch_token) { // Get operation (None, Some(payout_token)) => { - let (pm, supplementary_data) = - vault::Vault::get_payout_method_data_from_temporary_locker( + if payout_token.starts_with("temporary_token_") + || certain_payout_type == api_enums::PayoutType::Bank + { + let (pm, supplementary_data) = vault::Vault::get_payout_method_data_from_temporary_locker( state, &payout_token, merchant_key_store, @@ -79,15 +106,33 @@ pub async fn make_payout_method_data<'a>( .attach_printable( "Payout method for given token not found or there was a problem fetching it", )?; - utils::when( - supplementary_data - .customer_id - .ne(&Some(customer_id.to_owned())), - || { - Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payout method and customer passed in payout are not same".into() }) - }, - )?; - Ok(pm) + utils::when( + supplementary_data + .customer_id + .ne(&Some(customer_id.to_owned())), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { message: "customer associated with payout method and customer passed in payout are not same".into() }) + }, + )?; + Ok(pm) + } else { + let resp = cards::get_card_from_locker( + state, + customer_id, + merchant_id, + payout_token.as_ref(), + ) + .await + .attach_printable("Payout method [card] could not be fetched from HS locker")?; + Ok(Some({ + api::PayoutMethodData::Card(api::CardPayout { + card_number: resp.card_number, + expiry_month: resp.card_exp_month, + expiry_year: resp.card_exp_year, + card_holder_name: resp.name_on_card, + }) + })) + } } // Create / Update operation @@ -131,11 +176,11 @@ pub async fn save_payout_data_to_locker( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { - let (locker_req, card_details, payment_method_type) = match payout_method_data { + let (locker_req, card_details, bank_details, payment_method_type) = match payout_method_data { api_models::payouts::PayoutMethodData::Card(card) => { let card_detail = api::CardDetail { card_number: card.card_number.to_owned(), - card_holder_name: Some(card.card_holder_name.to_owned()), + card_holder_name: card.card_holder_name.to_owned(), card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), nick_name: None, @@ -145,7 +190,7 @@ pub async fn save_payout_data_to_locker( merchant_customer_id: payout_attempt.customer_id.to_owned(), card: transformers::Card { card_number: card.card_number.to_owned(), - name_on_card: Some(card.card_holder_name.to_owned()), + name_on_card: card.card_holder_name.to_owned(), card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), card_brand: None, @@ -157,6 +202,7 @@ pub async fn save_payout_data_to_locker( ( payload, Some(card_detail), + None, api_enums::PaymentMethodType::Debit, ) } @@ -191,6 +237,7 @@ pub async fn save_payout_data_to_locker( ( payload, None, + Some(bank.to_owned()), api_enums::PaymentMethodType::foreign_from(bank.to_owned()), ) } @@ -244,6 +291,7 @@ pub async fn save_payout_data_to_locker( payment_method_type: Some(payment_method_type), payment_method_issuer: None, payment_method_issuer_code: None, + bank_transfer: bank_details, card: card_details, metadata: None, customer_id: Some(payout_attempt.customer_id.to_owned()), diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 5acb66b5068e..ca852f832ee8 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,4 +1,3 @@ -use api_models::enums as api_enums; pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, @@ -9,9 +8,9 @@ pub use api_models::payment_methods::{ }; use error_stack::report; -use crate::{ - core::errors::{self, RouterResult}, - types::transformers::ForeignFrom, +use crate::core::{ + errors::{self, RouterResult}, + payments::helpers::validate_payment_method_type_against_payment_method, }; pub(crate) trait PaymentMethodCreateExt { @@ -21,16 +20,16 @@ pub(crate) trait PaymentMethodCreateExt { // convert self.payment_method_type to payment_method and compare it against self.payment_method impl PaymentMethodCreateExt for PaymentMethodCreate { fn validate(&self) -> RouterResult<()> { - let payment_method: Option = - self.payment_method_type.map(ForeignFrom::foreign_from); - if payment_method - .map(|payment_method| payment_method != self.payment_method) - .unwrap_or(false) - { - return Err(report!(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid 'payment_method_type' provided".to_string() - }) - .attach_printable("Invalid payment method type")); + if let Some(payment_method_type) = self.payment_method_type { + if !validate_payment_method_type_against_payment_method( + self.payment_method, + payment_method_type, + ) { + return Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid 'payment_method_type' provided".to_string() + }) + .attach_printable("Invalid payment method type")); + } } Ok(()) } diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 97dca3baa52b..490750805062 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -95,16 +95,16 @@ impl AdyenTest { card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), expiry_month: Secret::new("3".to_string()), expiry_year: Secret::new("2030".to_string()), - card_holder_name: Secret::new("John Doe".to_string()), + card_holder_name: Some(Secret::new("John Doe".to_string())), })) } enums::PayoutType::Bank => Some(api::PayoutMethodData::Bank( api::payouts::BankPayout::Sepa(api::SepaBankTransfer { iban: "NL46TEST0136169112".to_string().into(), bic: Some("ABNANL2A".to_string().into()), - bank_name: "Deutsche Bank".to_string(), - bank_country_code: enums::CountryAlpha2::NL, - bank_city: "Amsterdam".to_string(), + bank_name: Some("Deutsche Bank".to_string()), + bank_country_code: Some(enums::CountryAlpha2::NL), + bank_city: Some("Amsterdam".to_string()), }), )), }, diff --git a/crates/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index fb65397e1a22..de303523040e 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -73,9 +73,9 @@ impl WiseTest { api::BacsBankTransfer { bank_sort_code: "231470".to_string().into(), bank_account_number: "28821822".to_string().into(), - bank_name: "Deutsche Bank".to_string(), - bank_country_code: enums::CountryAlpha2::NL, - bank_city: "Amsterdam".to_string(), + bank_name: Some("Deutsche Bank".to_string()), + bank_country_code: Some(enums::CountryAlpha2::NL), + bank_city: Some("Amsterdam".to_string()), }, ))), ..Default::default() diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 466489e2f9f9..b2f5d3ea52c3 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2541,9 +2541,6 @@ "AchBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "bank_account_number", "bank_routing_number" ], @@ -2551,15 +2548,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "bank_account_number": { "type": "string", @@ -2993,9 +2997,6 @@ "BacsBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "bank_account_number", "bank_sort_code" ], @@ -3003,15 +3004,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "bank_account_number": { "type": "string", @@ -9139,6 +9147,14 @@ "description": "The card network", "example": "Visa", "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true } } }, @@ -9475,6 +9491,14 @@ "description": "A timestamp (ISO 8601 code) that determines when the customer was created", "example": "2023-01-18T11:04:09.922Z", "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true } } }, @@ -9585,6 +9609,14 @@ ], "nullable": true }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true + }, "metadata": { "type": "object", "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", @@ -12294,9 +12326,6 @@ "SepaBankTransfer": { "type": "object", "required": [ - "bank_name", - "bank_country_code", - "bank_city", "iban", "bic" ], @@ -12304,15 +12333,22 @@ "bank_name": { "type": "string", "description": "Bank name", - "example": "Deutsche Bank" + "example": "Deutsche Bank", + "nullable": true }, "bank_country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true }, "bank_city": { "type": "string", "description": "Bank city", - "example": "California" + "example": "California", + "nullable": true }, "iban": { "type": "string", From 68a3a280676c8309f9becffae545b134b5e1f2ea Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:07:41 +0530 Subject: [PATCH 339/443] feat(connector_events): added api to fetch connector event logs (#3319) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> --- crates/analytics/src/clickhouse.rs | 16 +++++ crates/analytics/src/connector_events.rs | 5 ++ crates/analytics/src/connector_events/core.rs | 27 ++++++++ .../analytics/src/connector_events/events.rs | 63 +++++++++++++++++++ crates/analytics/src/lib.rs | 1 + crates/analytics/src/sqlx.rs | 2 + crates/analytics/src/types.rs | 1 + crates/api_models/src/analytics.rs | 1 + .../src/analytics/connector_events.rs | 11 ++++ crates/api_models/src/events.rs | 5 +- crates/router/src/analytics.rs | 29 ++++++++- crates/router_env/src/lib.rs | 1 + 12 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 crates/analytics/src/connector_events.rs create mode 100644 crates/analytics/src/connector_events/core.rs create mode 100644 crates/analytics/src/connector_events/events.rs create mode 100644 crates/api_models/src/analytics/connector_events.rs diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index b8fd5e6a35d0..f81c29c801c0 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -21,6 +21,7 @@ use crate::{ filters::ApiEventFilter, metrics::{latency::LatencyAvg, ApiEventMetricRow}, }, + connector_events::events::ConnectorEventsResult, outgoing_webhook_event::events::OutgoingWebhookLogsResult, sdk_events::events::SdkEventsResult, types::TableEngine, @@ -121,6 +122,7 @@ impl AnalyticsDataSource for ClickhouseClient { } AnalyticsCollection::SdkEvents => TableEngine::BasicTree, AnalyticsCollection::ApiEvents => TableEngine::BasicTree, + AnalyticsCollection::ConnectorEvents => TableEngine::BasicTree, AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, } } @@ -147,6 +149,7 @@ impl super::sdk_events::events::SdkEventsFilterAnalytics for ClickhouseClient {} impl super::api_event::events::ApiLogsFilterAnalytics for ClickhouseClient {} impl super::api_event::filters::ApiEventFilterAnalytics for ClickhouseClient {} impl super::api_event::metrics::ApiEventMetricAnalytics for ClickhouseClient {} +impl super::connector_events::events::ConnectorEventLogAnalytics for ClickhouseClient {} impl super::outgoing_webhook_event::events::OutgoingWebhookLogsFilterAnalytics for ClickhouseClient { @@ -188,6 +191,18 @@ impl TryInto for serde_json::Value { } } +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self) + .into_report() + .change_context(ParsingError::StructParseFailure( + "Failed to parse ConnectorEventsResult in clickhouse results", + )) + } +} + impl TryInto for serde_json::Value { type Error = Report; @@ -344,6 +359,7 @@ impl ToSql for AnalyticsCollection { Self::SdkEvents => Ok("sdk_events_dist".to_string()), Self::ApiEvents => Ok("api_audit_log".to_string()), Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + Self::ConnectorEvents => Ok("connector_events_audit".to_string()), Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), } } diff --git a/crates/analytics/src/connector_events.rs b/crates/analytics/src/connector_events.rs new file mode 100644 index 000000000000..c7c31306a2c7 --- /dev/null +++ b/crates/analytics/src/connector_events.rs @@ -0,0 +1,5 @@ +mod core; +pub mod events; +pub trait ConnectorEventAnalytics: events::ConnectorEventLogAnalytics {} + +pub use self::core::connector_events_core; diff --git a/crates/analytics/src/connector_events/core.rs b/crates/analytics/src/connector_events/core.rs new file mode 100644 index 000000000000..15f841af5f82 --- /dev/null +++ b/crates/analytics/src/connector_events/core.rs @@ -0,0 +1,27 @@ +use api_models::analytics::connector_events::ConnectorEventsRequest; +use common_utils::errors::ReportSwitchExt; +use error_stack::{IntoReport, ResultExt}; + +use super::events::{get_connector_events, ConnectorEventsResult}; +use crate::{errors::AnalyticsResult, types::FiltersError, AnalyticsProvider}; + +pub async fn connector_events_core( + pool: &AnalyticsProvider, + req: ConnectorEventsRequest, + merchant_id: String, +) -> AnalyticsResult> { + let data = match pool { + AnalyticsProvider::Sqlx(_) => Err(FiltersError::NotImplemented( + "Connector Events not implemented for SQLX", + )) + .into_report() + .attach_printable("SQL Analytics is not implemented for Connector Events"), + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) => { + get_connector_events(&merchant_id, req, ckh_pool).await + } + } + .switch()?; + Ok(data) +} diff --git a/crates/analytics/src/connector_events/events.rs b/crates/analytics/src/connector_events/events.rs new file mode 100644 index 000000000000..096520777eeb --- /dev/null +++ b/crates/analytics/src/connector_events/events.rs @@ -0,0 +1,63 @@ +use api_models::analytics::{ + connector_events::{ConnectorEventsRequest, QueryType}, + Granularity, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, +}; +pub trait ConnectorEventLogAnalytics: LoadRow {} + +pub async fn get_connector_events( + merchant_id: &String, + query_param: ConnectorEventsRequest, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + ConnectorEventLogAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::ConnectorEvents); + query_builder.add_select_column("*").switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + match query_param.query_param { + QueryType::Payment { payment_id } => query_builder + .add_filter_clause("payment_id", payment_id) + .switch()?, + } + //TODO!: update the execute_query function to return reports instead of plain errors... + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ConnectorEventsResult { + pub merchant_id: String, + pub payment_id: String, + pub connector_name: Option, + pub request_id: Option, + pub flow: String, + pub request: String, + pub response: Option, + pub error: Option, + pub status_code: u16, + pub latency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + pub method: Option, +} diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 8529807a1a16..501bd58527c3 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -7,6 +7,7 @@ mod query; pub mod refunds; pub mod api_event; +pub mod connector_events; pub mod outgoing_webhook_event; pub mod sdk_events; mod sqlx; diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index e32b85a53672..7ab8a2aa4bc5 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -429,6 +429,8 @@ impl ToSql for AnalyticsCollection { Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) .attach_printable("ApiEvents table is not implemented for Sqlx"))?, Self::PaymentIntent => Ok("payment_intent".to_string()), + Self::ConnectorEvents => Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("ConnectorEvents table is not implemented for Sqlx"))?, Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, } diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 8da4655e255b..18e9e9f4334b 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -26,6 +26,7 @@ pub enum AnalyticsCollection { SdkEvents, ApiEvents, PaymentIntent, + ConnectorEvents, OutgoingWebhookEvent, } diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index e0d3fa671b60..c6ca215f9f7c 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -12,6 +12,7 @@ use self::{ pub use crate::payments::TimeRange; pub mod api_event; +pub mod connector_events; pub mod outgoing_webhook_event; pub mod payments; pub mod refunds; diff --git a/crates/api_models/src/analytics/connector_events.rs b/crates/api_models/src/analytics/connector_events.rs new file mode 100644 index 000000000000..b2974b0a3392 --- /dev/null +++ b/crates/api_models/src/analytics/connector_events.rs @@ -0,0 +1,11 @@ +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub enum QueryType { + Payment { payment_id: String }, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ConnectorEventsRequest { + #[serde(flatten)] + pub query_param: QueryType, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 26a9d222d6b9..43a72b7e3922 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -17,10 +17,12 @@ use common_utils::{ impl_misc_api_event_type, }; +#[allow(unused_imports)] use crate::{ admin::*, analytics::{ - api_event::*, outgoing_webhook_event::OutgoingWebhookLogsRequest, sdk_events::*, *, + api_event::*, connector_events::ConnectorEventsRequest, + outgoing_webhook_event::OutgoingWebhookLogsRequest, sdk_events::*, *, }, api_keys::*, cards_info::*, @@ -94,6 +96,7 @@ impl_misc_api_event_type!( GetApiEventMetricRequest, SdkEventsRequest, ReportRequest, + ConnectorEventsRequest, OutgoingWebhookLogsRequest ); diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index c62de5bd29ab..3f0febcc592c 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -3,7 +3,8 @@ pub use analytics::*; pub mod routes { use actix_web::{web, Responder, Scope}; use analytics::{ - api_event::api_events_core, errors::AnalyticsError, lambda_utils::invoke_lambda, + api_event::api_events_core, connector_events::connector_events_core, + errors::AnalyticsError, lambda_utils::invoke_lambda, outgoing_webhook_event::outgoing_webhook_events_core, sdk_events::sdk_events_core, }; use api_models::analytics::{ @@ -71,6 +72,10 @@ pub mod routes { ) .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) + .service( + web::resource("connector_event_logs") + .route(web::get().to(get_connector_events)), + ) .service( web::resource("outgoing_webhook_event_logs") .route(web::get().to(get_outgoing_webhook_events)), @@ -585,4 +590,26 @@ pub mod routes { )) .await } + + pub async fn get_connector_events( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Query, + ) -> impl Responder { + let flow = AnalyticsFlow::GetConnectorEvents; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| async move { + connector_events_core(&state.pool, req, auth.merchant_account.merchant_id) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } } diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index 0127d07170fd..9139b5eed417 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -52,6 +52,7 @@ pub enum AnalyticsFlow { GenerateRefundReport, GetApiEventMetrics, GetApiEventFilters, + GetConnectorEvents, GetOutgoingWebhookEvents, } From 387c1c491bdc413ae361d04f0be25eaa58e72fa9 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:28:49 +0530 Subject: [PATCH 340/443] refactor(connector): [cybersource] recurring mandate flow (#3354) --- crates/router/src/connector/cybersource.rs | 15 +- .../src/connector/cybersource/transformers.rs | 281 ++++++++++-------- crates/router/src/services/api.rs | 6 +- 3 files changed, 170 insertions(+), 132 deletions(-) diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index b300e97b44a9..ac2d16c9610e 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -785,7 +785,10 @@ impl ConnectorIntegration CustomResult { - if req.is_three_ds() && req.request.is_card() { + if req.is_three_ds() + && req.request.is_card() + && req.request.connector_mandate_id().is_none() + { Ok(format!( "{}risk/v1/authentication-setups", api::ConnectorCommon::base_url(self, connectors) @@ -809,7 +812,10 @@ impl ConnectorIntegration CustomResult { - if data.is_three_ds() && data.request.is_card() { + if data.is_three_ds() + && data.request.is_card() + && data.request.connector_mandate_id().is_none() + { let response: cybersource::CybersourceAuthSetupResponse = res .response .parse_struct("Cybersource AuthSetupResponse") diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index e83b23603e9b..8beb81d92368 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -342,7 +342,7 @@ pub struct ApplePayPaymentInformation { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MandatePaymentInformation { - payment_instrument: Option, + payment_instrument: CybersoucrePaymentInstrument, } #[derive(Debug, Serialize)] @@ -482,7 +482,7 @@ impl ), ) -> Self { let (action_list, action_token_types, authorization_options) = - if item.router_data.request.setup_future_usage.is_some() { + if item.router_data.request.setup_mandate_details.is_some() { ( Some(vec![CybersourceActionsList::TokenCreate]), Some(vec![CybersourceActionsTokenType::PaymentInstrument]), @@ -871,139 +871,168 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> fn try_from( item: &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { - payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), - payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - payments::WalletData::ApplePay(apple_pay_data) => { - match item.router_data.payment_method_token.clone() { - Some(payment_method_token) => match payment_method_token { - types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { - Self::try_from((item, decrypt_data)) - } - types::PaymentMethodToken::Token(_) => { - Err(errors::ConnectorError::InvalidWalletToken)? - } - }, - None => { - let email = item.router_data.request.get_email()?; - let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; - let order_information = OrderInformationWithBill::from((item, bill_to)); - let processing_information = ProcessingInformation::from(( - item, - Some(PaymentSolution::ApplePay), - )); - let client_reference_information = - ClientReferenceInformation::from(item); - let payment_information = PaymentInformation::ApplePayToken( - ApplePayTokenPaymentInformation { - fluid_data: FluidData { - value: Secret::from(apple_pay_data.payment_data), - }, - tokenized_card: ApplePayTokenizedCard { - transaction_type: TransactionType::ApplePay, - }, + match item.router_data.request.connector_mandate_id() { + Some(connector_mandate_id) => Self::try_from((item, connector_mandate_id)), + None => { + match item.router_data.request.payment_method_data.clone() { + payments::PaymentMethodData::Card(ccard) => Self::try_from((item, ccard)), + payments::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + payments::WalletData::ApplePay(apple_pay_data) => { + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + types::PaymentMethodToken::ApplePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data)) + } + types::PaymentMethodToken::Token(_) => { + Err(errors::ConnectorError::InvalidWalletToken)? + } }, - ); - let merchant_defined_information = - item.router_data.request.metadata.clone().map(|metadata| { - Vec::::foreign_from( - metadata.peek().to_owned(), - ) - }); - - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - merchant_defined_information, - consumer_authentication_information: None, - }) + None => { + let email = item.router_data.request.get_email()?; + let bill_to = + build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = + OrderInformationWithBill::from((item, bill_to)); + let processing_information = ProcessingInformation::from(( + item, + Some(PaymentSolution::ApplePay), + )); + let client_reference_information = + ClientReferenceInformation::from(item); + let payment_information = PaymentInformation::ApplePayToken( + ApplePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from(apple_pay_data.payment_data), + }, + tokenized_card: ApplePayTokenizedCard { + transaction_type: TransactionType::ApplePay, + }, + }, + ); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from( + metadata.peek().to_owned(), + ) + }); + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } + } + } + payments::WalletData::GooglePay(google_pay_data) => { + Self::try_from((item, google_pay_data)) } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::WeChatPayQr(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message( + "Cybersource", + ), + ) + .into()) + } + }, + // If connector_mandate_id is present MandatePayment will be the PMD, the case will be handled in the first `if` clause. + // This is a fallback implementation in the event of catastrophe. + payments::PaymentMethodData::MandatePayment => { + let connector_mandate_id = + item.router_data.request.connector_mandate_id().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "connector_mandate_id", + }, + )?; + Self::try_from((item, connector_mandate_id)) + } + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ) + .into()) } } - payments::WalletData::GooglePay(google_pay_data) => { - Self::try_from((item, google_pay_data)) - } - payments::WalletData::AliPayQr(_) - | payments::WalletData::AliPayRedirect(_) - | payments::WalletData::AliPayHkRedirect(_) - | payments::WalletData::MomoRedirect(_) - | payments::WalletData::KakaoPayRedirect(_) - | payments::WalletData::GoPayRedirect(_) - | payments::WalletData::GcashRedirect(_) - | payments::WalletData::ApplePayRedirect(_) - | payments::WalletData::ApplePayThirdPartySdk(_) - | payments::WalletData::DanaRedirect {} - | payments::WalletData::GooglePayRedirect(_) - | payments::WalletData::GooglePayThirdPartySdk(_) - | payments::WalletData::MbWayRedirect(_) - | payments::WalletData::MobilePayRedirect(_) - | payments::WalletData::PaypalRedirect(_) - | payments::WalletData::PaypalSdk(_) - | payments::WalletData::SamsungPay(_) - | payments::WalletData::TwintRedirect {} - | payments::WalletData::VippsRedirect {} - | payments::WalletData::TouchNGoRedirect(_) - | payments::WalletData::WeChatPayRedirect(_) - | payments::WalletData::WeChatPayQr(_) - | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Cybersource"), - ) - .into()), - }, - payments::PaymentMethodData::MandatePayment => { - let processing_information = ProcessingInformation::from((item, None)); - let payment_instrument = - item.router_data - .request - .connector_mandate_id() - .map(|mandate_token_id| CybersoucrePaymentInstrument { - id: mandate_token_id, - }); - - let email = item.router_data.request.get_email()?; - let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; - let order_information = OrderInformationWithBill::from((item, bill_to)); - let payment_information = - PaymentInformation::MandatePayment(MandatePaymentInformation { - payment_instrument, - }); - let client_reference_information = ClientReferenceInformation::from(item); - let merchant_defined_information = - item.router_data.request.metadata.clone().map(|metadata| { - Vec::::foreign_from(metadata.peek().to_owned()) - }); - Ok(Self { - processing_information, - payment_information, - order_information, - client_reference_information, - merchant_defined_information, - consumer_authentication_information: None, - }) - } - payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::PayLater(_) - | payments::PaymentMethodData::BankRedirect(_) - | payments::PaymentMethodData::BankDebit(_) - | payments::PaymentMethodData::BankTransfer(_) - | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) - | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) - | payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Cybersource"), - ) - .into()) } } } } +impl + TryFrom<( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + String, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, connector_mandate_id): ( + &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, + String, + ), + ) -> Result { + let processing_information = ProcessingInformation::from((item, None)); + let payment_instrument = CybersoucrePaymentInstrument { + id: connector_mandate_id, + }; + let email = item.router_data.request.get_email()?; + let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; + let order_information = OrderInformationWithBill::from((item, bill_to)); + let payment_information = + PaymentInformation::MandatePayment(MandatePaymentInformation { payment_instrument }); + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = + item.router_data.request.metadata.clone().map(|metadata| { + Vec::::foreign_from(metadata.peek().to_owned()) + }); + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + merchant_defined_information, + consumer_authentication_information: None, + }) + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceAuthSetupRequest { diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 9eb06d675a07..ad463fcf2b92 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1524,12 +1524,12 @@ pub fn build_redirection_form( // This is the iframe recommended by cybersource but the redirection happens inside this iframe once otp // is received and we lose control of the redirection on user client browser, so to avoid that we have removed this iframe and directly consumed it. // (PreEscaped(r#""#)) - (PreEscaped(format!("
- + (PreEscaped(format!(" +
"))) (PreEscaped(r#""#)) }} From 52f38d3d5a7d035e8211e1f51c8f982232e2d7ab Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:39:25 +0530 Subject: [PATCH 341/443] fix(core): add validation for authtype and metadata in update payment connector (#3305) --- crates/router/src/core/admin.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index e8593581126a..fd4cae3a2b9b 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1172,6 +1172,34 @@ pub async fn update_payment_connector( field_name: "connector_account_details".to_string(), expected_format: "auth_type and api_key".to_string(), })?; + let connector_name = mca.connector_name.as_ref(); + let connector_enum = api_models::enums::Connector::from_str(connector_name) + .into_report() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + validate_auth_and_metadata_type(connector_enum, &auth, &req.metadata).map_err( + |err| match *err.current_context() { + errors::ConnectorError::InvalidConnectorName => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The connector name is invalid".to_string(), + }) + } + errors::ConnectorError::InvalidConnectorConfig { config: field_name } => err + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: format!("The {} is invalid", field_name), + }), + errors::ConnectorError::FailedToObtainAuthType => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The auth type is invalid for the connector".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The request body is invalid".to_string(), + }), + }, + )?; let (connector_status, disabled) = validate_status_and_disabled(req.status, req.disabled, auth, mca.status)?; From 928beecdd7fe9e09b38ffe750627ca4af94ffc93 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:03:46 +0530 Subject: [PATCH 342/443] chore(router): remove recon from default features (#3370) Co-authored-by: Kashif --- crates/api_models/Cargo.toml | 2 +- crates/router/Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 45702a4ecb0a..d1f603f188eb 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -default = ["payouts", "frm", "recon"] +default = ["payouts", "frm"] business_profile_routing = [] connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 0a544e0bd090..8897fdac2c22 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,13 +9,13 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "recon"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen"] +release = ["kms", "stripe", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] @@ -30,7 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] -recon = ["email"] +recon = ["email", "api_models/recon"] retry = [] [dependencies] From ac8d81b32b3d91b875113d32782a8c62e39ba2a8 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Wed, 17 Jan 2024 16:31:22 +0530 Subject: [PATCH 343/443] fix(events): fix event generation for paymentmethods list (#3337) --- .github/CODEOWNERS | 11 +++++++++++ crates/api_models/src/events.rs | 1 - crates/api_models/src/events/payment.rs | 4 +++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a911d26d8650..0eb3d95bfc69 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,6 +23,17 @@ postman/ @juspay/hyperswitch-framework Cargo.toml @juspay/hyperswitch-framework Cargo.lock @juspay/hyperswitch-framework +crates/api_models/src/events/ @juspay/hyperswitch-analytics +crates/api_models/src/events.rs @juspay/hyperswitch-analytics +crates/api_models/src/analytics/ @juspay/hyperswitch-analytics +crates/api_models/src/analytics.rs @juspay/hyperswitch-analytics +crates/router/src/analytics.rs @juspay/hyperswitch-analytics +crates/router/src/events/ @juspay/hyperswitch-analytics +crates/router/src/events.rs @juspay/hyperswitch-analytics +crates/common_utils/src/events/ @juspay/hyperswitch-analytics +crates/common_utils/src/events.rs @juspay/hyperswitch-analytics +crates/analytics/ @juspay/hyperswitch-analytics + connector-template/ @juspay/hyperswitch-connector crates/router/src/connector/ @juspay/hyperswitch-connector crates/router/tests/connectors/ @juspay/hyperswitch-connector diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 43a72b7e3922..a8185d2d241c 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -39,7 +39,6 @@ impl ApiEventMetric for TimeRange {} impl_misc_api_event_type!( PaymentMethodId, PaymentsSessionResponse, - PaymentMethodListResponse, PaymentMethodCreate, PaymentLinkInitiateRequest, RetrievePaymentLinkResponse, diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index f718dc1ca4dd..32d3dc30bd8d 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -3,7 +3,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::{ payment_methods::{ CustomerPaymentMethodsListResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, - PaymentMethodResponse, PaymentMethodUpdate, + PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, @@ -119,6 +119,8 @@ impl ApiEventMetric for PaymentMethodListRequest { } } +impl ApiEventMetric for PaymentMethodListResponse {} + impl ApiEventMetric for PaymentListFilterConstraints { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) From eb2a61d8597995838f21b8233653c691118b2191 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:22:27 +0530 Subject: [PATCH 344/443] refactor: [Noon] adding new field max_amount to mandate request (#3209) Co-authored-by: AkshayaFoiger Co-authored-by: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> --- .../router/src/connector/noon/transformers.rs | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index bbf284848b59..81f3ab33e2f3 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -1,5 +1,5 @@ use common_utils::pii; -use error_stack::ResultExt; +use error_stack::{IntoReport, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -7,7 +7,7 @@ use crate::{ connector::utils::{ self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, }, - core::errors, + core::{errors, mandate::MandateBehaviour}, services, types::{self, api, storage::enums, transformers::ForeignFrom, ErrorResponse}, utils, @@ -30,11 +30,13 @@ pub enum NoonSubscriptionType { } #[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct NoonSubscriptionData { #[serde(rename = "type")] subscription_type: NoonSubscriptionType, //Short description about the subscription. name: String, + max_amount: String, } #[derive(Debug, Serialize)] @@ -91,7 +93,7 @@ pub struct NoonSubscription { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct NoonCard { - name_on_card: Secret, + name_on_card: Option>, number_plain: cards::CardNumber, expiry_month: Secret, expiry_year: Secret, @@ -158,7 +160,7 @@ pub struct NoonPayPal { } #[derive(Debug, Serialize)] -#[serde(tag = "type", content = "data")] +#[serde(tag = "type", content = "data", rename_all = "UPPERCASE")] pub enum NoonPaymentData { Card(NoonCard), Subscription(NoonSubscription), @@ -200,10 +202,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { _ => ( match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => Ok(NoonPaymentData::Card(NoonCard { - name_on_card: req_card - .card_holder_name - .clone() - .unwrap_or(Secret::new("".to_string())), + name_on_card: req_card.card_holder_name.clone(), number_plain: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), expiry_year: req_card.get_expiry_year_4_digit(), @@ -296,7 +295,11 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { } }?, Some(item.request.currency), - item.request.order_category.clone(), + Some(item.request.order_category.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "order_category", + }, + )?), ), }; @@ -330,17 +333,33 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { }, }); - let (subscription, tokenize_c_c) = - match item.request.setup_future_usage.is_some().then_some(( - NoonSubscriptionData { - subscription_type: NoonSubscriptionType::Unscheduled, - name: name.clone(), - }, - true, - )) { - Some((a, b)) => (Some(a), Some(b)), - None => (None, None), - }; + let subscription = item + .request + .get_setup_mandate_details() + .map(|mandate_data| { + let max_amount = match &mandate_data.mandate_type { + Some(data_models::mandates::MandateDataType::SingleUse(mandate)) + | Some(data_models::mandates::MandateDataType::MultiUse(Some(mandate))) => { + conn_utils::to_currency_base_unit(mandate.amount, mandate.currency) + } + _ => Err(errors::ConnectorError::MissingRequiredField { + field_name: "setup_future_usage.mandate_data.mandate_type", + }) + .into_report(), + }?; + + Ok::>( + NoonSubscriptionData { + subscription_type: NoonSubscriptionType::Unscheduled, + name: name.clone(), + max_amount, + }, + ) + }) + .transpose()?; + + let tokenize_c_c = subscription.is_some().then_some(true); + let order = NoonOrder { amount: conn_utils::to_currency_base_unit(item.request.amount, item.request.currency)?, currency, From ee0461e9a57b2ff77d26b4a543bd1e407f3fcc1c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:20:02 +0000 Subject: [PATCH 345/443] chore(version): 2024.01.18.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b01fe4e915..b17ee4964b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.18.0 + +### Features + +- **connector_events:** Added api to fetch connector event logs ([#3319](https://github.com/juspay/hyperswitch/pull/3319)) ([`68a3a28`](https://github.com/juspay/hyperswitch/commit/68a3a280676c8309f9becffae545b134b5e1f2ea)) +- **payment_method:** Add capability to store bank details using /payment_methods endpoint ([#3113](https://github.com/juspay/hyperswitch/pull/3113)) ([`01c2de2`](https://github.com/juspay/hyperswitch/commit/01c2de223f60595d77c06a59a40dfe041e02cfee)) + +### Bug Fixes + +- **core:** Add validation for authtype and metadata in update payment connector ([#3305](https://github.com/juspay/hyperswitch/pull/3305)) ([`52f38d3`](https://github.com/juspay/hyperswitch/commit/52f38d3d5a7d035e8211e1f51c8f982232e2d7ab)) +- **events:** Fix event generation for paymentmethods list ([#3337](https://github.com/juspay/hyperswitch/pull/3337)) ([`ac8d81b`](https://github.com/juspay/hyperswitch/commit/ac8d81b32b3d91b875113d32782a8c62e39ba2a8)) + +### Refactors + +- **connector:** [cybersource] recurring mandate flow ([#3354](https://github.com/juspay/hyperswitch/pull/3354)) ([`387c1c4`](https://github.com/juspay/hyperswitch/commit/387c1c491bdc413ae361d04f0be25eaa58e72fa9)) +- [Noon] adding new field max_amount to mandate request ([#3209](https://github.com/juspay/hyperswitch/pull/3209)) ([`eb2a61d`](https://github.com/juspay/hyperswitch/commit/eb2a61d8597995838f21b8233653c691118b2191)) + +### Miscellaneous Tasks + +- **router:** Remove recon from default features ([#3370](https://github.com/juspay/hyperswitch/pull/3370)) ([`928beec`](https://github.com/juspay/hyperswitch/commit/928beecdd7fe9e09b38ffe750627ca4af94ffc93)) + +**Full Changelog:** [`2024.01.17.0...2024.01.18.0`](https://github.com/juspay/hyperswitch/compare/2024.01.17.0...2024.01.18.0) + +- - - + ## 2024.01.17.0 ### Features From acb329672297cd7337d0b0239e4c662257812e8a Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:27:32 +0530 Subject: [PATCH 346/443] refactor(connector): [Volt] Refactor Payments and Refunds Webhooks (#3377) --- crates/router/src/connector/volt.rs | 71 +++++++++---------- .../router/src/connector/volt/transformers.rs | 10 ++- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/crates/router/src/connector/volt.rs b/crates/router/src/connector/volt.rs index 39296bb64340..f125f90d93aa 100644 --- a/crates/router/src/connector/volt.rs +++ b/crates/router/src/connector/volt.rs @@ -635,43 +635,40 @@ impl api::IncomingWebhook for Volt { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let x_volt_type = - utils::get_header_key_value(webhook_headers::X_VOLT_TYPE, request.headers)?; - if x_volt_type == "refund_confirmed" || x_volt_type == "refund_failed" { - let refund_webhook_body: volt::VoltRefundWebhookBodyReference = request - .body - .parse_struct("VoltRefundWebhookBodyReference") - .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; - - let refund_reference = match refund_webhook_body.external_reference { - Some(external_reference) => { - api_models::webhooks::RefundIdType::RefundId(external_reference) - } - None => api_models::webhooks::RefundIdType::ConnectorRefundId( - refund_webhook_body.refund, - ), - }; - Ok(api_models::webhooks::ObjectReferenceId::RefundId( - refund_reference, - )) - } else { - let webhook_body: volt::VoltPaymentWebhookBodyReference = request - .body - .parse_struct("VoltPaymentWebhookBodyReference") - .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; - let reference = match webhook_body.merchant_internal_reference { - Some(merchant_internal_reference) => { - api_models::payments::PaymentIdType::PaymentAttemptId( - merchant_internal_reference, - ) - } - None => api_models::payments::PaymentIdType::ConnectorTransactionId( - webhook_body.payment, - ), - }; - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - reference, - )) + let parsed_webhook_response = request + .body + .parse_struct::("VoltRefundWebhookBodyReference") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + match parsed_webhook_response { + volt::WebhookResponse::Payment(payment_response) => { + let reference = match payment_response.merchant_internal_reference { + Some(merchant_internal_reference) => { + api_models::payments::PaymentIdType::PaymentAttemptId( + merchant_internal_reference, + ) + } + None => api_models::payments::PaymentIdType::ConnectorTransactionId( + payment_response.payment, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + reference, + )) + } + volt::WebhookResponse::Refund(refund_response) => { + let refund_reference = match refund_response.external_reference { + Some(external_reference) => { + api_models::webhooks::RefundIdType::RefundId(external_reference) + } + None => api_models::webhooks::RefundIdType::ConnectorRefundId( + refund_response.refund, + ), + }; + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + refund_reference, + )) + } } } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index 8b9bbecb0889..b1d77f3416ff 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -46,7 +46,6 @@ pub mod webhook_headers { pub const X_VOLT_SIGNED: &str = "X-Volt-Signed"; pub const X_VOLT_TIMED: &str = "X-Volt-Timed"; pub const USER_AGENT: &str = "User-Agent"; - pub const X_VOLT_TYPE: &str = "X-Volt-Type"; } #[derive(Debug, Serialize)] @@ -488,6 +487,15 @@ pub struct VoltRefundWebhookBodyReference { pub external_reference: Option, } +#[derive(Debug, Deserialize, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum WebhookResponse { + // the enum order shouldn't be changed as this is being used during serialization and deserialization + Refund(VoltRefundWebhookBodyReference), + Payment(VoltPaymentWebhookBodyReference), +} + #[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum VoltWebhookBodyEventType { From 2f693ad1fd857280ef30c6cc0297fb926f0e79e8 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:51:47 +0530 Subject: [PATCH 347/443] fix(user): fetch profile_id for sample data (#3358) --- crates/router/src/utils/user/sample_data.rs | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index dcf635595e0f..3fa2a10629e5 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -52,7 +52,7 @@ pub async fn generate_sample_data( let business_label_default = merchant_parsed_details.first().map(|x| x.business.clone()); - let profile_id = crate::core::utils::get_profile_id_from_business_details( + let profile_id = match crate::core::utils::get_profile_id_from_business_details( business_country_default, business_label_default.as_ref(), &merchant_from_db, @@ -61,8 +61,25 @@ pub async fn generate_sample_data( false, ) .await - .change_context(SampleDataError::InternalServerError) - .attach_printable("Failed to get business profile")?; + { + Ok(id) => id.clone(), + Err(error) => { + router_env::logger::error!( + "Profile ID not found in business details. Attempting to fetch from the database {error:?}" + ); + + state + .store + .list_business_profile_by_merchant_id(&merchant_id) + .await + .change_context(SampleDataError::InternalServerError) + .attach_printable("Failed to get business profile")? + .first() + .ok_or(SampleDataError::InternalServerError)? + .profile_id + .clone() + } + }; // 10 percent payments should be failed #[allow(clippy::as_conversions)] From e816ccfbdd7b0e24464aa93421e399d63f23b17c Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 18 Jan 2024 14:24:10 +0530 Subject: [PATCH 348/443] fix(connector): Trustpay zen error mapping (#3255) Co-authored-by: Prasunna Soppa Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connector/trustpay/transformers.rs | 48 ++++++- .../router/src/connector/zen/transformers.rs | 127 ++++++++++++++---- 2 files changed, 140 insertions(+), 35 deletions(-) diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 87d98c1b1bee..4d8e47ab0dc3 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -813,7 +813,7 @@ fn handle_bank_redirects_sync_response( errors::ConnectorError, > { let status = enums::AttemptStatus::from(response.payment_information.status); - let error = if status == enums::AttemptStatus::AuthorizationFailed { + let error = if utils::is_payment_failure(status) { let reason_info = response .payment_information .status_reason_information @@ -856,6 +856,7 @@ fn handle_bank_redirects_sync_response( pub fn handle_webhook_response( payment_information: WebhookPaymentInformation, + status_code: u16, ) -> CustomResult< ( enums::AttemptStatus, @@ -865,6 +866,22 @@ pub fn handle_webhook_response( errors::ConnectorError, > { let status = enums::AttemptStatus::try_from(payment_information.status)?; + let error = if utils::is_payment_failure(status) { + let reason_info = payment_information + .status_reason_information + .unwrap_or_default(); + Some(types::ErrorResponse { + code: reason_info.reason.code.clone(), + // message vary for the same code, so relying on code alone as it is unique + message: reason_info.reason.code, + reason: reason_info.reason.reject_reason, + status_code, + attempt_status: None, + connector_transaction_id: payment_information.references.payment_request_id.clone(), + }) + } else { + None + }; let payment_response_data = types::PaymentsResponseData::TransactionResponse { resource_id: types::ResponseId::NoResponseId, redirection_data: None, @@ -874,7 +891,7 @@ pub fn handle_webhook_response( connector_response_reference_id: None, incremental_authorization_allowed: None, }; - Ok((status, None, payment_response_data)) + Ok((status, error, payment_response_data)) } pub fn get_trustpay_response( @@ -901,7 +918,9 @@ pub fn get_trustpay_response( TrustpayPaymentsResponse::BankRedirectError(response) => { handle_bank_redirects_error_response(*response, status_code) } - TrustpayPaymentsResponse::WebhookResponse(response) => handle_webhook_response(*response), + TrustpayPaymentsResponse::WebhookResponse(response) => { + handle_webhook_response(*response, status_code) + } } } @@ -1452,9 +1471,24 @@ fn handle_cards_refund_response( fn handle_webhooks_refund_response( response: WebhookPaymentInformation, + status_code: u16, ) -> CustomResult<(Option, types::RefundsResponseData), errors::ConnectorError> { let refund_status = diesel_models::enums::RefundStatus::try_from(response.status)?; + let error = if utils::is_refund_failure(refund_status) { + let reason_info = response.status_reason_information.unwrap_or_default(); + Some(types::ErrorResponse { + code: reason_info.reason.code.clone(), + // message vary for the same code, so relying on code alone as it is unique + message: reason_info.reason.code, + reason: reason_info.reason.reject_reason, + status_code, + attempt_status: None, + connector_transaction_id: response.references.payment_request_id.clone(), + }) + } else { + None + }; let refund_response_data = types::RefundsResponseData { connector_refund_id: response .references @@ -1462,7 +1496,7 @@ fn handle_webhooks_refund_response( .ok_or(errors::ConnectorError::MissingConnectorRefundID)?, refund_status, }; - Ok((None, refund_response_data)) + Ok((error, refund_response_data)) } fn handle_bank_redirects_refund_response( @@ -1495,7 +1529,7 @@ fn handle_bank_redirects_refund_sync_response( status_code: u16, ) -> (Option, types::RefundsResponseData) { let refund_status = enums::RefundStatus::from(response.payment_information.status); - let error = if refund_status == enums::RefundStatus::Failure { + let error = if utils::is_refund_failure(refund_status) { let reason_info = response .payment_information .status_reason_information @@ -1551,7 +1585,9 @@ impl TryFrom> RefundResponse::CardsRefund(response) => { handle_cards_refund_response(*response, item.http_code)? } - RefundResponse::WebhookRefund(response) => handle_webhooks_refund_response(*response)?, + RefundResponse::WebhookRefund(response) => { + handle_webhooks_refund_response(*response, item.http_code)? + } RefundResponse::BankRedirectRefund(response) => { handle_bank_redirects_refund_response(*response, item.http_code) } diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 7ea6953a3f2e..0adae0d00bdb 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -11,6 +11,7 @@ use crate::{ connector::utils::{ self, BrowserInformationData, CardData, PaymentsAuthorizeRequestData, RouterData, }, + consts, core::errors::{self, CustomResult}, services::{self, Method}, types::{self, api, storage::enums, transformers::ForeignTryFrom}, @@ -848,12 +849,15 @@ impl ForeignTryFrom<(ZenPaymentStatus, Option)> for enums::AttemptSt } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApiResponse { status: ZenPaymentStatus, id: String, + // merchant_transaction_id: Option, merchant_action: Option, + reject_code: Option, + reject_reason: Option, } #[derive(Debug, Deserialize)] @@ -869,18 +873,18 @@ pub struct CheckoutResponse { redirect_url: url::Url, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ZenMerchantAction { action: ZenActions, data: ZenMerchantActionData, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum ZenActions { Redirect, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ZenMerchantActionData { redirect_url: url::Url, @@ -913,6 +917,57 @@ impl } } +fn get_zen_response( + response: ApiResponse, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let redirection_data_action = response.merchant_action.map(|merchant_action| { + ( + services::RedirectForm::from((merchant_action.data.redirect_url, Method::Get)), + merchant_action.action, + ) + }); + let (redirection_data, action) = match redirection_data_action { + Some((redirect_form, action)) => (Some(redirect_form), Some(action)), + None => (None, None), + }; + let status = enums::AttemptStatus::foreign_try_from((response.status, action))?; + let error = if utils::is_payment_failure(status) { + Some(types::ErrorResponse { + code: response + .reject_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .reject_reason + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.reject_reason, + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(response.id.clone()), + }) + } else { + None + }; + let payment_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(response.id.clone()), + redirection_data, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + }; + Ok((status, error, payment_response_data)) +} + impl TryFrom> for types::RouterData { @@ -920,28 +975,12 @@ impl TryFrom, ) -> Result { - let redirection_data_action = value.response.merchant_action.map(|merchant_action| { - ( - services::RedirectForm::from((merchant_action.data.redirect_url, Method::Get)), - merchant_action.action, - ) - }); - let (redirection_data, action) = match redirection_data_action { - Some((redirect_form, action)) => (Some(redirect_form), Some(action)), - None => (None, None), - }; + let (status, error, payment_response_data) = + get_zen_response(value.response.clone(), value.http_code)?; Ok(Self { - status: enums::AttemptStatus::foreign_try_from((value.response.status, action))?, - response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(value.response.id), - redirection_data, - mandate_reference: None, - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - }), + status, + response: error.map_or_else(|| Ok(payment_response_data), Err), ..value.data }) } @@ -1016,9 +1055,12 @@ impl From for enums::RefundStatus { } #[derive(Default, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RefundResponse { id: String, status: RefundStatus, + reject_code: Option, + reject_reason: Option, } impl TryFrom> @@ -1028,17 +1070,44 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let refund_status = enums::RefundStatus::from(item.response.status); + let (error, refund_response_data) = get_zen_refund_response(item.response, item.http_code)?; Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id, - refund_status, - }), + response: error.map_or_else(|| Ok(refund_response_data), Err), ..item.data }) } } +fn get_zen_refund_response( + response: RefundResponse, + status_code: u16, +) -> CustomResult<(Option, types::RefundsResponseData), errors::ConnectorError> +{ + let refund_status = enums::RefundStatus::from(response.status); + let error = if utils::is_refund_failure(refund_status) { + Some(types::ErrorResponse { + code: response + .reject_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: response + .reject_reason + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: response.reject_reason, + status_code, + attempt_status: None, + connector_transaction_id: Some(response.id.clone()), + }) + } else { + None + }; + let refund_response_data = types::RefundsResponseData { + connector_refund_id: response.id, + refund_status, + }; + Ok((error, refund_response_data)) +} + impl TryFrom> for types::RefundsRouterData { From b4df40db25f6ea743c7a25db47e8f1d8e0d544e3 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:44:59 +0530 Subject: [PATCH 349/443] fix(metrics): Add TASKS_ADDED_COUNT and TASKS_RESET_COUNT metrics in router scheduler flow (#3189) --- crates/router/src/core/api_keys.rs | 5 +++++ .../router/src/core/payment_methods/cards.rs | 8 ++++++++ .../router/src/core/payment_methods/vault.rs | 13 +++++++++++- crates/router/src/core/payments/helpers.rs | 20 +++++++++++++++++-- crates/router/src/core/refunds.rs | 16 ++++++++++++++- crates/router/src/routes/metrics.rs | 3 +++ crates/router/src/workflows/api_key_expiry.rs | 6 ++++++ crates/scheduler/src/metrics.rs | 2 -- 8 files changed, 67 insertions(+), 6 deletions(-) diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index c1ddc43cd65d..78d4e801e8f2 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -270,6 +270,11 @@ pub async fn add_api_key_expiry_task( api_key_expiry_tracker.key_id ) })?; + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "ApiKeyExpiry")], + ); Ok(()) } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 51f543536350..0dbf0680d14b 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -2963,6 +2963,14 @@ impl TempLockerCardSupport { ) .await?; metrics::TOKENIZED_DATA_COUNT.add(&metrics::CONTEXT, 1, &[]); + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + "DeleteTokenizeData", + )], + ); Ok(card) } } diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 063b69687577..5b783f1c5d4e 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1027,7 +1027,18 @@ pub async fn retry_delete_tokenize( let schedule_time = get_delete_tokenize_schedule_time(db, pm, pt.retry_count).await; match schedule_time { - Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, + Some(s_time) => { + let retry_schedule = pt.retry(db.as_scheduler(), s_time).await; + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + "DeleteTokenizeData", + )], + ); + retry_schedule + } None => { pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 92dc1bf5f4b3..46e1e15fe717 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -986,14 +986,30 @@ where match schedule_time { Some(stime) => { if !requeue { - // scheduler_metrics::TASKS_ADDED_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics + // Here, increment the count of added tasks every time a payment has been confirmed or PSync has been called + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + format!("{:#?}", operation), + )], + ); super::add_process_sync_task(&*state.store, payment_attempt, stime) .await .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while adding task to process tracker") } else { - // scheduler_metrics::TASKS_RESET_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics + // When the requeue is true, we reset the tasks count as we reset the task every time it is requeued + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes( + "flow", + format!("{:#?}", operation), + )], + ); super::reset_process_sync_task(&*state.store, payment_attempt, stime) .await .into_report() diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index e60c341dedcf..4b1c33296e65 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -1088,6 +1088,12 @@ pub async fn add_refund_sync_task( refund.refund_id ) })?; + metrics::TASKS_ADDED_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "Refund")], + ); + Ok(response) } @@ -1170,7 +1176,15 @@ pub async fn retry_refund_sync_task( get_refund_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count).await?; match schedule_time { - Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, + Some(s_time) => { + let retry_schedule = pt.retry(db.as_scheduler(), s_time).await; + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "Refund")], + ); + retry_schedule + } None => { pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) .await diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index b3629ab7d52b..6c3293dba9d0 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -113,5 +113,8 @@ counter_metric!(AUTO_RETRY_GSM_MATCH_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_EXHAUSTED_COUNT, GLOBAL_METER); counter_metric!(AUTO_RETRY_PAYMENT_COUNT, GLOBAL_METER); +counter_metric!(TASKS_ADDED_COUNT, GLOBAL_METER); // Tasks added to process tracker +counter_metric!(TASKS_RESET_COUNT, GLOBAL_METER); // Tasks reset in process tracker for requeue flow + pub mod request; pub mod utils; diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs index 62d8a54c4024..eb3c1d9c1ce9 100644 --- a/crates/router/src/workflows/api_key_expiry.rs +++ b/crates/router/src/workflows/api_key_expiry.rs @@ -115,6 +115,12 @@ Team Hyperswitch"), let task_ids = vec![task_id]; db.process_tracker_update_process_status_by_ids(task_ids, updated_process_tracker_data) .await?; + // Remaining tasks are re-scheduled, so will be resetting the added count + metrics::TASKS_RESET_COUNT.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("flow", "ApiKeyExpiry")], + ); } Ok(()) diff --git a/crates/scheduler/src/metrics.rs b/crates/scheduler/src/metrics.rs index 134f5599b31d..ca4fb9ec2424 100644 --- a/crates/scheduler/src/metrics.rs +++ b/crates/scheduler/src/metrics.rs @@ -6,8 +6,6 @@ global_meter!(PT_METER, "PROCESS_TRACKER"); histogram_metric!(CONSUMER_STATS, PT_METER, "CONSUMER_OPS"); counter_metric!(PAYMENT_COUNT, PT_METER); // No. of payments created -counter_metric!(TASKS_ADDED_COUNT, PT_METER); // Tasks added to process tracker -counter_metric!(TASKS_RESET_COUNT, PT_METER); // Tasks reset in process tracker for requeue flow counter_metric!(TASKS_PICKED_COUNT, PT_METER); // Tasks picked by counter_metric!(BATCHES_CREATED, PT_METER); // Batches added to stream counter_metric!(BATCHES_CONSUMED, PT_METER); // Batches consumed by consumer From 059e86607dc271c25bb3d23f5adfc7d5f21f62fb Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:54:27 +0530 Subject: [PATCH 350/443] fix(connector): [Cashtocode] update amount from i64 to f64 in webhook payload (#3382) --- crates/router/src/connector/cashtocode/transformers.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/cashtocode/transformers.rs b/crates/router/src/connector/cashtocode/transformers.rs index 9aa6286a963f..8a92956756a8 100644 --- a/crates/router/src/connector/cashtocode/transformers.rs +++ b/crates/router/src/connector/cashtocode/transformers.rs @@ -193,7 +193,7 @@ pub struct CashtocodePaymentsResponseData { #[serde(rename_all = "camelCase")] pub struct CashtocodePaymentsSyncResponse { pub transaction_id: String, - pub amount: i64, + pub amount: f64, } fn get_redirect_form_data( @@ -314,7 +314,6 @@ impl connector_response_reference_id: None, incremental_authorization_allowed: None, }), - amount_captured: Some(item.response.amount), ..item.data }) } @@ -330,7 +329,7 @@ pub struct CashtocodeErrorResponse { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CashtocodeIncomingWebhook { - pub amount: i64, + pub amount: f64, pub currency: String, pub foreign_transaction_id: String, #[serde(rename = "type")] From bd5356e7e7cf61f9d07fe9b67c9c5bb38fddf9c7 Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:10:21 +0530 Subject: [PATCH 351/443] refactor(core): add locker config to enable or disable locker (#3352) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 1 + crates/api_models/src/mandates.rs | 11 ++ crates/api_models/src/payment_methods.rs | 41 +++++++ crates/router/src/configs/defaults.rs | 2 + crates/router/src/configs/settings.rs | 1 + crates/router/src/core/locker_migration.rs | 4 + crates/router/src/core/mandate.rs | 18 ++-- .../router/src/core/payment_methods/cards.rs | 82 +++++++++++--- .../src/core/payment_methods/transformers.rs | 40 +++++-- .../src/core/payments/flows/authorize_flow.rs | 4 +- .../core/payments/flows/setup_mandate_flow.rs | 1 + crates/router/src/core/payments/helpers.rs | 52 +++++---- .../router/src/core/payments/tokenization.rs | 102 ++++++++++++++++-- crates/router/src/core/payouts/helpers.rs | 80 +++++++++++--- crates/router/src/core/webhooks.rs | 10 +- crates/router/src/routes/customers.rs | 7 +- crates/router/src/routes/mandates.rs | 6 +- crates/router/src/routes/payment_methods.rs | 2 +- crates/router/src/types/api/mandates.rs | 58 +++++++--- loadtest/config/development.toml | 1 + openapi/openapi_spec.json | 77 +++++++++++++ 23 files changed, 505 insertions(+), 98 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index d4e119641922..cf25ef195a24 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -131,6 +131,7 @@ host_rs = "" # Rust Locker host mock_locker = true # Emulate a locker locally using Postgres basilisk_host = "" # Basilisk host locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +locker_enabled = true # Boolean to enable or disable saving cards in locker [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response diff --git a/config/development.toml b/config/development.toml index 91269005a0f0..b23f68680e64 100644 --- a/config/development.toml +++ b/config/development.toml @@ -69,6 +69,8 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true + [forex_api] call_delay = 21600 diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 450fe106a31f..8af1528e1771 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -56,6 +56,7 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true [jwekey] vault_encryption_key = "" diff --git a/crates/api_models/src/mandates.rs b/crates/api_models/src/mandates.rs index 5c0810dc21be..b29f4e0d0c34 100644 --- a/crates/api_models/src/mandates.rs +++ b/crates/api_models/src/mandates.rs @@ -36,6 +36,8 @@ pub struct MandateResponse { pub payment_method_id: String, /// The payment method pub payment_method: String, + /// The payment method type + pub payment_method_type: Option, /// The card details for mandate pub card: Option, /// Details about the customer’s acceptance @@ -66,6 +68,15 @@ pub struct MandateCardDetails { #[schema(value_type = Option)] /// A unique identifier alias to identify a particular card pub card_fingerprint: Option>, + /// The first 6 digits of card + pub card_isin: Option, + /// The bank that issued the card + pub card_issuer: Option, + /// The network that facilitates payment card transactions + #[schema(value_type = Option)] + pub card_network: Option, + /// The type of the payment card + pub card_type: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 3467777da745..984e6dbffff9 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -109,6 +109,19 @@ pub struct CardDetail { /// Card Holder's Nick Name #[schema(value_type = Option,example = "John Doe")] pub nick_name: Option>, + + /// Card Issuing Country + pub card_issuing_country: Option, + + /// Card's Network + #[schema(value_type = Option)] + pub card_network: Option, + + /// Issuer Bank for Card + pub card_issuer: Option, + + /// Card Type + pub card_type: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -177,6 +190,12 @@ pub struct CardDetailsPaymentMethod { pub expiry_year: Option>, pub nick_name: Option>, pub card_holder_name: Option>, + pub card_isin: Option, + pub card_issuer: Option, + pub card_network: Option, + pub card_type: Option, + #[serde(default = "saved_in_locker_default")] + pub saved_to_locker: bool, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -227,6 +246,18 @@ pub struct CardDetailFromLocker { #[schema(value_type=Option)] pub nick_name: Option>, + + #[schema(value_type = Option)] + pub card_network: Option, + + pub card_isin: Option, + pub card_issuer: Option, + pub card_type: Option, + pub saved_to_locker: bool, +} + +fn saved_in_locker_default() -> bool { + true } impl From for CardDetailFromLocker { @@ -242,6 +273,11 @@ impl From for CardDetailFromLocker { card_holder_name: item.card_holder_name, card_fingerprint: None, nick_name: item.nick_name, + card_isin: item.card_isin, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type, + saved_to_locker: item.saved_to_locker, } } } @@ -255,6 +291,11 @@ impl From for CardDetailsPaymentMethod { expiry_year: item.expiry_year, nick_name: item.nick_name, card_holder_name: item.card_holder_name, + card_isin: item.card_isin, + card_issuer: item.card_issuer, + card_network: item.card_network, + card_type: item.card_type, + saved_to_locker: item.saved_to_locker, } } } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 42839bf35131..e4a470d0da35 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -54,6 +54,8 @@ impl Default for super::settings::Locker { mock_locker: true, basilisk_host: "localhost".into(), locker_signing_key_id: "1".into(), + //true or false + locker_enabled: true, } } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 3d93c2f188b7..bcf26d63ae8d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -490,6 +490,7 @@ pub struct Locker { pub mock_locker: bool, pub basilisk_host: String, pub locker_signing_key_id: String, + pub locker_enabled: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index e3e308a8a01c..4bd2555792a2 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -101,6 +101,10 @@ pub async fn call_to_locker( card_exp_year: card.card_exp_year, card_holder_name: card.name_on_card, nick_name: card.nick_name.map(masking::Secret::new), + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, }; let pm_create = api::PaymentMethodCreate { diff --git a/crates/router/src/core/mandate.rs b/crates/router/src/core/mandate.rs index aabd846660ca..b6837d14f829 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -33,6 +33,7 @@ use crate::{ pub async fn get_mandate( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: mandates::MandateId, ) -> RouterResponse { let mandate = state @@ -42,7 +43,7 @@ pub async fn get_mandate( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; Ok(services::ApplicationResponse::Json( - mandates::MandateResponse::from_db_mandate(&state, mandate).await?, + mandates::MandateResponse::from_db_mandate(&state, key_store, mandate).await?, )) } @@ -202,6 +203,7 @@ pub async fn update_connector_mandate_id( pub async fn get_customer_mandates( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, req: customers::CustomerId, ) -> RouterResponse> { let mandates = state @@ -221,7 +223,10 @@ pub async fn get_customer_mandates( } else { let mut response_vec = Vec::with_capacity(mandates.len()); for mandate in mandates { - response_vec.push(mandates::MandateResponse::from_db_mandate(&state, mandate).await?); + response_vec.push( + mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate) + .await?, + ); } Ok(services::ApplicationResponse::Json(response_vec)) } @@ -383,6 +388,7 @@ where pub async fn retrieve_mandates_list( state: AppState, merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, constraints: api_models::mandates::MandateListConstraints, ) -> RouterResponse> { let mandates = state @@ -392,11 +398,9 @@ pub async fn retrieve_mandates_list( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to retrieve mandates")?; - let mandates_list = future::try_join_all( - mandates - .into_iter() - .map(|mandate| mandates::MandateResponse::from_db_mandate(&state, mandate)), - ) + let mandates_list = future::try_join_all(mandates.into_iter().map(|mandate| { + mandates::MandateResponse::from_db_mandate(&state, key_store.clone(), mandate) + })) .await?; Ok(services::ApplicationResponse::Json(mandates_list)) } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 0dbf0680d14b..712ae9e4035e 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -558,6 +558,7 @@ pub async fn add_card_hs( req, &merchant_account.merchant_id, ); + Ok(( payment_method_resp, store_card_payload.duplicate.unwrap_or(false), @@ -2508,11 +2509,19 @@ pub async fn list_customer_payment_method( let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); let (card, pmd, hyperswitch_token_data) = match pm.payment_method { - enums::PaymentMethod::Card => ( - Some(get_card_details(&pm, key, state).await?), - None, - PaymentTokenData::permanent_card(pm.payment_method_id.clone()), - ), + enums::PaymentMethod::Card => { + let card_details = get_card_details_with_locker_fallback(&pm, key, state).await?; + + if card_details.is_some() { + ( + card_details, + None, + PaymentTokenData::permanent_card(pm.payment_method_id.clone()), + ) + } else { + continue; + } + } #[cfg(feature = "payouts")] enums::PaymentMethod::BankTransfer => { @@ -2571,6 +2580,7 @@ pub async fn list_customer_payment_method( }; //Need validation for enabled payment method ,querying MCA + let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), customer_id: pm.customer_id, @@ -2700,7 +2710,38 @@ pub async fn list_customer_payment_method( Ok(services::ApplicationResponse::Json(response)) } -async fn get_card_details( +pub async fn get_card_details_with_locker_fallback( + pm: &payment_method::PaymentMethod, + key: &[u8], + state: &routes::AppState, +) -> errors::RouterResult> { + let card_decrypted = + decrypt::(pm.payment_method_data.clone(), key) + .await + .change_context(errors::StorageError::DecryptionError) + .attach_printable("unable to decrypt card details") + .ok() + .flatten() + .map(|x| x.into_inner().expose()) + .and_then(|v| serde_json::from_value::(v).ok()) + .and_then(|pmd| match pmd { + PaymentMethodsData::Card(crd) => Some(api::CardDetailFromLocker::from(crd)), + _ => None, + }); + + Ok(if let Some(mut crd) = card_decrypted { + if crd.saved_to_locker { + crd.scheme = pm.scheme.clone(); + Some(crd) + } else { + None + } + } else { + Some(get_card_details_from_locker(state, pm).await?) + }) +} + +pub async fn get_card_details_without_locker_fallback( pm: &payment_method::PaymentMethod, key: &[u8], state: &routes::AppState, @@ -2979,25 +3020,32 @@ impl TempLockerCardSupport { pub async fn retrieve_payment_method( state: routes::AppState, pm: api::PaymentMethodId, + key_store: domain::MerchantKeyStore, ) -> errors::RouterResponse { let db = state.store.as_ref(); let pm = db .find_payment_method(&pm.payment_method_id) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + + let key = key_store.key.peek(); let card = if pm.payment_method == enums::PaymentMethod::Card { - let card = get_card_from_locker( - &state, - &pm.customer_id, - &pm.merchant_id, - &pm.payment_method_id, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error getting card from card vault")?; - let card_detail = payment_methods::get_card_detail(&pm, card) + let card_detail = if state.conf.locker.locker_enabled { + let card = get_card_from_locker( + &state, + &pm.customer_id, + &pm.merchant_id, + &pm.payment_method_id, + ) + .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details from locker")?; + .attach_printable("Error getting card from card vault")?; + payment_methods::get_card_detail(&pm, card) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card details from locker")? + } else { + get_card_details_without_locker_fallback(&pm, key, &state).await? + }; Some(card_detail) } else { None diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index da4f03b49c1e..304091e42ac7 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -352,18 +352,26 @@ pub fn mk_add_card_response_hs( req: api::PaymentMethodCreate, merchant_id: &str, ) -> api::PaymentMethodResponse { - let mut card_number = card.card_number.peek().to_owned(); + let card_number = card.card_number.clone(); + let last4_digits = card_number.clone().get_last4(); + let card_isin = card_number.get_card_isin(); + let card = api::CardDetailFromLocker { scheme: None, - last4_digits: Some(card_number.split_off(card_number.len() - 4)), - issuer_country: None, // [#256] bin mapping - card_number: Some(card.card_number), - expiry_month: Some(card.card_exp_month), - expiry_year: Some(card.card_exp_year), - card_token: None, // [#256] - card_fingerprint: None, // fingerprint not send by basilisk-hs need to have this feature in case we need it in future - card_holder_name: card.card_holder_name, - nick_name: card.nick_name, + last4_digits: Some(last4_digits), + issuer_country: None, + card_number: Some(card.card_number.clone()), + expiry_month: Some(card.card_exp_month.clone()), + expiry_year: Some(card.card_exp_year.clone()), + card_token: None, + card_fingerprint: None, + card_holder_name: card.card_holder_name.clone(), + nick_name: card.nick_name.clone(), + card_isin: Some(card_isin), + card_issuer: card.card_issuer, + card_network: card.card_network, + card_type: card.card_type, + saved_to_locker: true, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -399,6 +407,11 @@ pub fn mk_add_card_response( card_fingerprint: Some(response.card_fingerprint), card_holder_name: card.card_holder_name, nick_name: card.nick_name, + card_isin: None, + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -597,6 +610,8 @@ pub fn get_card_detail( ) -> CustomResult { let card_number = response.card_number; let mut last4_digits = card_number.peek().to_owned(); + //fetch form card bin + let card_detail = api::CardDetailFromLocker { scheme: pm.scheme.to_owned(), issuer_country: pm.issuer_country.clone(), @@ -608,6 +623,11 @@ pub fn get_card_detail( card_fingerprint: None, card_holder_name: response.name_on_card, nick_name: response.nick_name.map(masking::Secret::new), + card_isin: None, + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, }; Ok(card_detail) } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 07af15a336d9..15c79f4b9d95 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -92,7 +92,9 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics - if resp.request.setup_mandate_details.clone().is_some() { + let is_mandate = resp.request.setup_mandate_details.is_some(); + + if is_mandate { let payment_method_id = Box::pin(tokenization::save_payment_method( state, connector, diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index 0c03c8ce123b..d6343ed871b0 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -208,6 +208,7 @@ impl types::SetupMandateRouterData { .to_setup_mandate_failed_response()?; let payment_method_type = self.request.payment_method_type; + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 46e1e15fe717..9d3da6c78e4a 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -509,16 +509,23 @@ pub async fn get_token_for_recurring_mandate( }; if let diesel_models::enums::PaymentMethod::Card = payment_method.payment_method { - let _ = - cards::get_lookup_key_from_locker(state, &token, &payment_method, merchant_key_store) - .await?; + if state.conf.locker.locker_enabled { + let _ = cards::get_lookup_key_from_locker( + state, + &token, + &payment_method, + merchant_key_store, + ) + .await?; + } + if let Some(payment_method_from_request) = req.payment_method { let pm: storage_enums::PaymentMethod = payment_method_from_request; if pm != payment_method.payment_method { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: "payment method in request does not match previously provided payment \ - method information" + method information" .into() }))? } @@ -971,7 +978,6 @@ pub fn payment_intent_status_fsm( None => storage_enums::IntentStatus::RequiresPaymentMethod, } } - pub async fn add_domain_task_to_pt( operation: &Op, state: &AppState, @@ -1050,6 +1056,10 @@ pub(crate) async fn get_payment_method_create_request( card_exp_year: card.card_exp_year.clone(), card_holder_name: card.card_holder_name.clone(), nick_name: card.nick_name.clone(), + card_issuing_country: card.card_issuing_country.clone(), + card_network: card.card_network.clone(), + card_issuer: card.card_issuer.clone(), + card_type: card.card_type.clone(), }; let customer_id = customer.customer_id.clone(); let payment_method_request = api::PaymentMethodCreate { @@ -3359,21 +3369,23 @@ pub async fn get_additional_payment_data( }, )) }); - card_info.unwrap_or(api_models::payments::AdditionalPaymentData::Card(Box::new( - api_models::payments::AdditionalCardInfo { - card_issuer: None, - card_network: None, - bank_code: None, - card_type: None, - card_issuing_country: None, - last4, - card_isin, - card_extended_bin, - card_exp_month: Some(card_data.card_exp_month.clone()), - card_exp_year: Some(card_data.card_exp_year.clone()), - card_holder_name: card_data.card_holder_name.clone(), - }, - ))) + card_info.unwrap_or_else(|| { + api_models::payments::AdditionalPaymentData::Card(Box::new( + api_models::payments::AdditionalCardInfo { + card_issuer: None, + card_network: None, + bank_code: None, + card_type: None, + card_issuing_country: None, + last4, + card_isin, + card_extended_bin, + card_exp_month: Some(card_data.card_exp_month.clone()), + card_exp_year: Some(card_data.card_exp_year.clone()), + card_holder_name: card_data.card_holder_name.clone(), + }, + )) + }) } } api_models::payments::PaymentMethodData::BankRedirect(bank_redirect_data) => { diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index f884cb79e7e1..15d88c94660c 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -14,7 +14,7 @@ use crate::{ services, types::{ self, - api::{self, CardDetailsPaymentMethod, PaymentMethodCreateExt}, + api::{self, CardDetailFromLocker, CardDetailsPaymentMethod, PaymentMethodCreateExt}, domain, storage::enums as storage_enums, }, @@ -74,12 +74,21 @@ where .await?; let merchant_id = &merchant_account.merchant_id; - let locker_response = save_in_locker( - state, - merchant_account, - payment_method_create_request.to_owned(), - ) - .await?; + let locker_response = if !state.conf.locker.locker_enabled { + skip_saving_card_in_locker( + merchant_account, + payment_method_create_request.to_owned(), + ) + .await? + } else { + save_in_locker( + state, + merchant_account, + payment_method_create_request.to_owned(), + ) + .await? + }; + let is_duplicate = locker_response.1; let pm_card_details = locker_response.0.card.as_ref().map(|card| { @@ -168,6 +177,85 @@ where } } +async fn skip_saving_card_in_locker( + merchant_account: &domain::MerchantAccount, + payment_method_request: api::PaymentMethodCreate, +) -> RouterResult<(api_models::payment_methods::PaymentMethodResponse, bool)> { + let merchant_id = &merchant_account.merchant_id; + let customer_id = payment_method_request + .clone() + .customer_id + .clone() + .get_required_value("customer_id")?; + let payment_method_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm"); + + let last4_digits = payment_method_request + .card + .clone() + .map(|c| c.card_number.get_last4()); + + let card_isin = payment_method_request + .card + .clone() + .map(|c: api_models::payment_methods::CardDetail| c.card_number.get_card_isin()); + + match payment_method_request.card.clone() { + Some(card) => { + let card_detail = CardDetailFromLocker { + scheme: None, + issuer_country: card.card_issuing_country.clone(), + last4_digits: last4_digits.clone(), + card_number: None, + expiry_month: Some(card.card_exp_month.clone()), + expiry_year: Some(card.card_exp_year), + card_token: None, + card_holder_name: card.card_holder_name.clone(), + card_fingerprint: None, + nick_name: None, + card_isin: card_isin.clone(), + card_issuer: card.card_issuer.clone(), + card_network: card.card_network.clone(), + card_type: card.card_type.clone(), + saved_to_locker: false, + }; + let pm_resp = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id), + payment_method_id, + payment_method: payment_method_request.payment_method, + payment_method_type: payment_method_request.payment_method_type, + card: Some(card_detail), + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + metadata: None, + created: Some(common_utils::date_time::now()), + bank_transfer: None, + }; + + Ok((pm_resp, false)) + } + None => { + let pm_id = common_utils::generate_id(crate::consts::ID_LENGTH, "pm"); + let payment_method_response = api::PaymentMethodResponse { + merchant_id: merchant_id.to_string(), + customer_id: Some(customer_id), + payment_method_id: pm_id, + payment_method: payment_method_request.payment_method, + payment_method_type: payment_method_request.payment_method_type, + card: None, + metadata: None, + created: Some(common_utils::date_time::now()), + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + bank_transfer: None, + }; + Ok((payment_method_response, false)) + } + } +} + pub async fn save_in_locker( state: &AppState, merchant_account: &domain::MerchantAccount, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 56e3a6faf537..1ab24023bdb8 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1,6 +1,6 @@ use common_utils::{ errors::CustomResult, - ext_traits::{StringExt, ValueExt}, + ext_traits::{AsyncExt, StringExt, ValueExt}, }; use diesel_models::encryption::Encryption; use error_stack::{IntoReport, ResultExt}; @@ -19,6 +19,7 @@ use crate::{ }, db::StorageInterface, routes::AppState, + services, types::{ api::{self, enums as api_enums}, domain::{ @@ -184,6 +185,10 @@ pub async fn save_payout_data_to_locker( card_exp_month: card.expiry_month.to_owned(), card_exp_year: card.expiry_year.to_owned(), nick_name: None, + card_issuing_country: None, + card_network: None, + card_issuer: None, + card_type: None, }; let payload = StoreLockerReq::LockerCard(StoreCardReq { merchant_id: &merchant_account.merchant_id, @@ -267,20 +272,65 @@ pub async fn save_payout_data_to_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payouts in saved payout method")?; - let pm_data = api::payment_methods::PaymentMethodsData::Card( - api::payment_methods::CardDetailsPaymentMethod { - last4_digits: card_details - .as_ref() - .map(|c| c.card_number.clone().get_last4()), - issuer_country: None, - expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), - expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), - nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), - card_holder_name: card_details - .as_ref() - .and_then(|c| c.card_holder_name.clone()), - }, - ); + // fetch card info from db + let card_isin = card_details + .as_ref() + .map(|c| c.card_number.clone().get_card_isin()); + + let pm_data = card_isin + .clone() + .async_and_then(|card_isin| async move { + db.get_card_info(&card_isin) + .await + .map_err(|error| services::logger::warn!(card_info_error=?error)) + .ok() + }) + .await + .flatten() + .map(|card_info| { + api::payment_methods::PaymentMethodsData::Card( + api::payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details + .as_ref() + .map(|c| c.card_number.clone().get_last4()), + issuer_country: card_info.card_issuing_country, + expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), + expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), + nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), + card_holder_name: card_details + .as_ref() + .and_then(|c| c.card_holder_name.clone()), + + card_isin: card_isin.clone(), + card_issuer: card_info.card_issuer, + card_network: card_info.card_network, + card_type: card_info.card_type, + saved_to_locker: true, + }, + ) + }) + .unwrap_or_else(|| { + api::payment_methods::PaymentMethodsData::Card( + api::payment_methods::CardDetailsPaymentMethod { + last4_digits: card_details + .as_ref() + .map(|c| c.card_number.clone().get_last4()), + issuer_country: None, + expiry_month: card_details.as_ref().map(|c| c.card_exp_month.clone()), + expiry_year: card_details.as_ref().map(|c| c.card_exp_year.clone()), + nick_name: card_details.as_ref().and_then(|c| c.nick_name.clone()), + card_holder_name: card_details + .as_ref() + .and_then(|c| c.card_holder_name.clone()), + + card_isin: card_isin.clone(), + card_issuer: None, + card_network: None, + card_type: None, + saved_to_locker: true, + }, + ) + }); let card_details_encrypted = cards::create_encrypted_payment_method_data(key_store, Some(pm_data)).await; diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 4354a3ee1959..f291d1cd2e80 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -421,6 +421,7 @@ pub async fn mandates_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, + key_store: domain::MerchantKeyStore, webhook_details: api::IncomingWebhookDetails, source_verified: bool, event_type: api_models::webhooks::IncomingWebhookEvent, @@ -464,8 +465,12 @@ pub async fn mandates_incoming_webhook_flow( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; let mandates_response = Box::new( - api::mandates::MandateResponse::from_db_mandate(&state, updated_mandate.clone()) - .await?, + api::mandates::MandateResponse::from_db_mandate( + &state, + key_store, + updated_mandate.clone(), + ) + .await?, ); let event_type: Option = updated_mandate.mandate_status.foreign_into(); if let Some(outgoing_event_type) = event_type { @@ -1237,6 +1242,7 @@ pub async fn webhooks_core RouterResult; + async fn from_db_mandate( + state: &AppState, + key_store: domain::MerchantKeyStore, + mandate: storage::Mandate, + ) -> RouterResult; } #[async_trait::async_trait] impl MandateResponseExt for MandateResponse { - async fn from_db_mandate(state: &AppState, mandate: storage::Mandate) -> RouterResult { + async fn from_db_mandate( + state: &AppState, + key_store: domain::MerchantKeyStore, + mandate: storage::Mandate, + ) -> RouterResult { let db = &*state.store; let payment_method = db .find_payment_method(&mandate.payment_method_id) @@ -36,21 +45,35 @@ impl MandateResponseExt for MandateResponse { .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; let card = if payment_method.payment_method == storage_enums::PaymentMethod::Card { - let card = payment_methods::cards::get_card_from_locker( - state, - &payment_method.customer_id, - &payment_method.merchant_id, - &payment_method.payment_method_id, - ) - .await?; - let card_detail = payment_methods::transformers::get_card_detail(&payment_method, card) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting card details")?; - Some(MandateCardDetails::from(card_detail).into_inner()) + // if locker is disabled , decrypt the payment method data + let card_details = if state.conf.locker.locker_enabled { + let card = payment_methods::cards::get_card_from_locker( + state, + &payment_method.customer_id, + &payment_method.merchant_id, + &payment_method.payment_method_id, + ) + .await?; + + payment_methods::transformers::get_card_detail(&payment_method, card) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting card details")? + } else { + payment_methods::cards::get_card_details_without_locker_fallback( + &payment_method, + key_store.key.get_inner().peek(), + state, + ) + .await? + }; + + Some(MandateCardDetails::from(card_details).into_inner()) } else { None }; - + let payment_method_type = payment_method + .payment_method_type + .map(|pmt| pmt.to_string()); Ok(Self { mandate_id: mandate.mandate_id, customer_acceptance: Some(api::payments::CustomerAcceptance { @@ -68,6 +91,7 @@ impl MandateResponseExt for MandateResponse { card, status: mandate.mandate_status, payment_method: payment_method.payment_method.to_string(), + payment_method_type, payment_method_id: mandate.payment_method_id, }) } @@ -84,6 +108,10 @@ impl From for MandateCardDetails { scheme: card_details_from_locker.scheme, issuer_country: card_details_from_locker.issuer_country, card_fingerprint: card_details_from_locker.card_fingerprint, + card_isin: card_details_from_locker.card_isin, + card_issuer: card_details_from_locker.card_issuer, + card_network: card_details_from_locker.card_network, + card_type: card_details_from_locker.card_type, } .into() } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 358a591a6678..268ebd1d3ac9 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -33,6 +33,7 @@ host = "" host_rs = "" mock_locker = true basilisk_host = "" +locker_enabled = true [forex_api] call_delay = 21600 diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index b2f5d3ea52c3..02df6324a06d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4421,11 +4421,37 @@ "description": "Card Holder's Nick Name", "example": "John Doe", "nullable": true + }, + "card_issuing_country": { + "type": "string", + "description": "Card Issuing Country", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "Issuer Bank for Card", + "nullable": true + }, + "card_type": { + "type": "string", + "description": "Card Type", + "nullable": true } } }, "CardDetailFromLocker": { "type": "object", + "required": [ + "saved_to_locker" + ], "properties": { "scheme": { "type": "string", @@ -4462,6 +4488,29 @@ "nick_name": { "type": "string", "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_isin": { + "type": "string", + "nullable": true + }, + "card_issuer": { + "type": "string", + "nullable": true + }, + "card_type": { + "type": "string", + "nullable": true + }, + "saved_to_locker": { + "type": "boolean" } } }, @@ -6884,6 +6933,29 @@ "type": "string", "description": "A unique identifier alias to identify a particular card", "nullable": true + }, + "card_isin": { + "type": "string", + "description": "The first 6 digits of card", + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "The bank that issued the card", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "description": "The type of the payment card", + "nullable": true } } }, @@ -6932,6 +7004,11 @@ "type": "string", "description": "The payment method" }, + "payment_method_type": { + "type": "string", + "description": "The payment method type", + "nullable": true + }, "card": { "allOf": [ { From 975986d9666cc4dc67aabc3f6576edf6804bb11a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:11:30 +0000 Subject: [PATCH 352/443] chore(version): 2024.01.18.1 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b17ee4964b41..fbc63ebaf975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.18.1 + +### Bug Fixes + +- **connector:** + - Trustpay zen error mapping ([#3255](https://github.com/juspay/hyperswitch/pull/3255)) ([`e816ccf`](https://github.com/juspay/hyperswitch/commit/e816ccfbdd7b0e24464aa93421e399d63f23b17c)) + - [Cashtocode] update amount from i64 to f64 in webhook payload ([#3382](https://github.com/juspay/hyperswitch/pull/3382)) ([`059e866`](https://github.com/juspay/hyperswitch/commit/059e86607dc271c25bb3d23f5adfc7d5f21f62fb)) +- **metrics:** Add TASKS_ADDED_COUNT and TASKS_RESET_COUNT metrics in router scheduler flow ([#3189](https://github.com/juspay/hyperswitch/pull/3189)) ([`b4df40d`](https://github.com/juspay/hyperswitch/commit/b4df40db25f6ea743c7a25db47e8f1d8e0d544e3)) +- **user:** Fetch profile_id for sample data ([#3358](https://github.com/juspay/hyperswitch/pull/3358)) ([`2f693ad`](https://github.com/juspay/hyperswitch/commit/2f693ad1fd857280ef30c6cc0297fb926f0e79e8)) + +### Refactors + +- **connector:** [Volt] Refactor Payments and Refunds Webhooks ([#3377](https://github.com/juspay/hyperswitch/pull/3377)) ([`acb3296`](https://github.com/juspay/hyperswitch/commit/acb329672297cd7337d0b0239e4c662257812e8a)) +- **core:** Add locker config to enable or disable locker ([#3352](https://github.com/juspay/hyperswitch/pull/3352)) ([`bd5356e`](https://github.com/juspay/hyperswitch/commit/bd5356e7e7cf61f9d07fe9b67c9c5bb38fddf9c7)) + +**Full Changelog:** [`2024.01.18.0...2024.01.18.1`](https://github.com/juspay/hyperswitch/compare/2024.01.18.0...2024.01.18.1) + +- - - + ## 2024.01.18.0 ### Features From 862a1b5303ff304cca41d3553f652fd1091aab9b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:44:20 +0530 Subject: [PATCH 353/443] feat(users): Add `preferred_merchant_id` column and update user details API (#3373) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 6 ++- crates/api_models/src/user.rs | 6 +++ crates/diesel_models/src/query/user_role.rs | 14 ++++++ crates/diesel_models/src/schema.rs | 2 + crates/diesel_models/src/user.rs | 7 +++ crates/router/src/core/user.rs | 46 +++++++++++++++++++ crates/router/src/db/kafka_store.rs | 14 ++++++ crates/router/src/db/user.rs | 5 ++ crates/router/src/db/user_role.rs | 43 +++++++++++++++++ crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/user.rs | 18 ++++++++ crates/router_env/src/logger/types.rs | 2 + .../down.sql | 2 + .../up.sql | 2 + 15 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql create mode 100644 migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index c0743c8b8fc0..40d082d1cade 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -13,7 +13,8 @@ use crate::user::{ AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest, - SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, VerifyEmailRequest, + SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, + UserMerchantCreate, VerifyEmailRequest, }; impl ApiEventMetric for DashboardEntryResponse { @@ -54,7 +55,8 @@ common_utils::impl_misc_api_event_type!( InviteUserRequest, InviteUserResponse, VerifyEmailRequest, - SendVerifyEmailRequest + SendVerifyEmailRequest, + UpdateUserAccountDetailsRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index a04c4fef6601..8de6a3c0b4fa 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -147,3 +147,9 @@ pub struct VerifyTokenResponse { pub merchant_id: String, pub user_email: pii::Email, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UpdateUserAccountDetailsRequest { + pub name: Option>, + pub preferred_merchant_id: Option, +} diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index d2f9564a5309..6b408038ef55 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -19,6 +19,20 @@ impl UserRole { .await } + pub async fn find_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + ) + .await + } + pub async fn update_by_user_id_merchant_id( conn: &PgPooledConn, user_id: String, diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 131d2b182661..c9887e1770fc 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1056,6 +1056,8 @@ diesel::table! { is_verified -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + preferred_merchant_id -> Nullable, } } diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index c608f2654c6a..84fe8710060e 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -19,6 +19,7 @@ pub struct User { pub is_verified: bool, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub preferred_merchant_id: Option, } #[derive( @@ -33,6 +34,7 @@ pub struct UserNew { pub is_verified: bool, pub created_at: Option, pub last_modified_at: Option, + pub preferred_merchant_id: Option, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -42,6 +44,7 @@ pub struct UserUpdateInternal { password: Option>, is_verified: Option, last_modified_at: PrimitiveDateTime, + preferred_merchant_id: Option, } #[derive(Debug)] @@ -51,6 +54,7 @@ pub enum UserUpdate { name: Option, password: Option>, is_verified: Option, + preferred_merchant_id: Option, }, } @@ -63,16 +67,19 @@ impl From for UserUpdateInternal { password: None, is_verified: Some(true), last_modified_at, + preferred_merchant_id: None, }, UserUpdate::AccountUpdate { name, password, is_verified, + preferred_merchant_id, } => Self { name, password, is_verified, last_modified_at, + preferred_merchant_id, }, } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 27a4f67618e4..729cef65c20a 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -253,6 +253,7 @@ pub async fn change_password( name: None, password: Some(new_password_hash), is_verified: None, + preferred_merchant_id: None, }, ) .await @@ -330,6 +331,7 @@ pub async fn reset_password( name: None, password: Some(hash_password), is_verified: Some(true), + preferred_merchant_id: None, }, ) .await @@ -786,3 +788,47 @@ pub async fn verify_token( user_email: user.email, })) } + +pub async fn update_user_details( + state: AppState, + user_token: auth::UserFromToken, + req: user_api::UpdateUserAccountDetailsRequest, +) -> UserResponse<()> { + let user: domain::UserFromStorage = state + .store + .find_user_by_id(&user_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + let name = req.name.map(domain::UserName::new).transpose()?; + + if let Some(ref preferred_merchant_id) = req.preferred_merchant_id { + let _ = state + .store + .find_user_role_by_user_id_merchant_id(user.get_user_id(), preferred_merchant_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::MerchantIdNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + } + + let user_update = storage_user::UserUpdate::AccountUpdate { + name: name.map(|x| x.get_secret().expose()), + password: None, + is_verified: None, + preferred_merchant_id: req.preferred_merchant_id, + }; + + state + .store + .update_user_by_user_id(user.get_user_id(), user_update) + .await + .change_context(UserErrors::InternalServerError)?; + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 19a83088a06f..8398c153156d 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1927,12 +1927,24 @@ impl UserRoleInterface for KafkaStore { ) -> CustomResult { self.diesel_store.insert_user_role(user_role).await } + async fn find_user_role_by_user_id( &self, user_id: &str, ) -> CustomResult { self.diesel_store.find_user_role_by_user_id(user_id).await } + + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .find_user_role_by_user_id_merchant_id(user_id, merchant_id) + .await + } + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -1943,9 +1955,11 @@ impl UserRoleInterface for KafkaStore { .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) .await } + async fn delete_user_role(&self, user_id: &str) -> CustomResult { self.diesel_store.delete_user_role(user_id).await } + async fn list_user_roles_by_user_id( &self, user_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index e3dda965f9c9..ecd71f7e2c9b 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -145,6 +145,7 @@ impl UserInterface for MockDb { is_verified: user_data.is_verified, created_at: user_data.created_at.unwrap_or(time_now), last_modified_at: user_data.created_at.unwrap_or(time_now), + preferred_merchant_id: user_data.preferred_merchant_id, }; users.push(user.clone()); Ok(user) @@ -207,10 +208,14 @@ impl UserInterface for MockDb { name, password, is_verified, + preferred_merchant_id, } => storage::User { name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), password: password.clone().unwrap_or(user.password.clone()), is_verified: is_verified.unwrap_or(user.is_verified), + preferred_merchant_id: preferred_merchant_id + .clone() + .or(user.preferred_merchant_id.clone()), ..user.to_owned() }, }; diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index bf84ae134ea7..d8938f9683da 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -14,16 +14,25 @@ pub trait UserRoleInterface { &self, user_role: storage::UserRoleNew, ) -> CustomResult; + async fn find_user_role_by_user_id( &self, user_id: &str, ) -> CustomResult; + + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, merchant_id: &str, update: storage::UserRoleUpdate, ) -> CustomResult; + async fn delete_user_role(&self, user_id: &str) -> CustomResult; async fn list_user_roles_by_user_id( @@ -57,6 +66,22 @@ impl UserRoleInterface for Store { .into_report() } + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::UserRole::find_by_user_id_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + ) + .await + .map_err(Into::into) + .into_report() + } + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, @@ -148,6 +173,24 @@ impl UserRoleInterface for MockDb { ) } + async fn find_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let user_roles = self.user_roles.lock().await; + user_roles + .iter() + .find(|user_role| user_role.user_id == user_id && user_role.merchant_id == merchant_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user role available for user_id = {user_id} and merchant_id = {merchant_id}" + )) + .into(), + ) + } + async fn update_user_role_by_user_id_merchant_id( &self, user_id: &str, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0c489dbe63a7..0807fb0800e1 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -921,6 +921,7 @@ impl User { .service(web::resource("/role/list").route(web::get().to(list_roles))) .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) .service(web::resource("/user/invite").route(web::post().to(invite_user))) + .service(web::resource("/update").route(web::post().to(update_user_account_details))) .service( web::resource("/data") .route(web::get().to(get_multiple_dashboard_metadata)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 12cf76be4759..805fb1152647 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -178,7 +178,8 @@ impl From for ApiIdentifier { | Flow::InviteUser | Flow::UserSignUpWithMerchantId | Flow::VerifyEmail - | Flow::VerifyEmailRequest => Self::User, + | Flow::VerifyEmailRequest + | Flow::UpdateUserAccountDetails => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 976fd5c9f564..eca32318adf6 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -403,3 +403,21 @@ pub async fn verify_recon_token(state: web::Data, http_req: HttpReques )) .await } + +pub async fn update_user_account_details( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UpdateUserAccountDetails; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + user_core::update_user_details, + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0d6636e567da..c4e0aa3f3ea2 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -331,6 +331,8 @@ pub enum Flow { VerifyEmail, /// Send verify email VerifyEmailRequest, + /// Update user account details + UpdateUserAccountDetails, } /// diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql new file mode 100644 index 000000000000..b9160b6f1052 --- /dev/null +++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN preferred_merchant_id; diff --git a/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql new file mode 100644 index 000000000000..77567ce93faf --- /dev/null +++ b/migrations/2024-01-02-111223_users_preferred_merchant_column/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64); From 7516a16763877c03ecc35fda19388bbd021c5cc7 Mon Sep 17 00:00:00 2001 From: Rachit Naithani <81706961+racnan@users.noreply.github.com> Date: Thu, 18 Jan 2024 20:18:44 +0530 Subject: [PATCH 354/443] feat(users): Added get role from jwt api (#3385) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/user_role.rs | 2 + crates/router/src/core/user_role.rs | 18 +++- crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 8 +- crates/router/src/routes/user_role.rs | 16 +++- .../router/src/services/authorization/info.rs | 26 +++--- .../src/services/authorization/permissions.rs | 64 +++++++------- crates/router/src/types/domain/user.rs | 29 +++---- crates/router/src/utils/user_role.rs | 85 +++++++++---------- crates/router_env/src/logger/types.rs | 2 + 10 files changed, 140 insertions(+), 111 deletions(-) diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 72fca2b2f084..b057f8ca8bce 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -43,6 +43,7 @@ pub enum Permission { SurchargeDecisionManagerRead, UsersRead, UsersWrite, + MerchantAccountCreate, } #[derive(Debug, serde::Serialize)] @@ -60,6 +61,7 @@ pub enum PermissionModule { Files, ThreeDsDecisionManager, SurchargeDecisionManager, + AccountCreate, } #[derive(Debug, serde::Serialize)] diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 2b7752d1904b..d8ff836e1f88 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -20,7 +20,7 @@ pub async fn get_authorization_info( user_role_api::AuthorizationInfoResponse( info::get_authorization_info() .into_iter() - .filter_map(|module| module.try_into().ok()) + .map(Into::into) .collect(), ), )) @@ -63,6 +63,22 @@ pub async fn get_role( Ok(ApplicationResponse::Json(info)) } +pub async fn get_role_from_token( + _state: AppState, + user: auth::UserFromToken, +) -> UserResponse> { + Ok(ApplicationResponse::Json( + predefined_permissions::PREDEFINED_PERMISSIONS + .get(user.role_id.as_str()) + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Invalid Role Id in JWT")? + .get_permissions() + .iter() + .map(|&per| per.into()) + .collect(), + )) +} + pub async fn update_user_role( state: AppState, user_from_token: auth::UserFromToken, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0807fb0800e1..3d63df2fe800 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -919,6 +919,7 @@ impl User { .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) + .service(web::resource("/role").route(web::get().to(get_role_from_token))) .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) .service(web::resource("/user/invite").route(web::post().to(invite_user))) .service(web::resource("/update").route(web::post().to(update_user_account_details))) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 805fb1152647..d3a2e1af9a71 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -181,9 +181,11 @@ impl From for ApiIdentifier { | Flow::VerifyEmailRequest | Flow::UpdateUserAccountDetails => Self::User, - Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { - Self::UserRole - } + Flow::ListRoles + | Flow::GetRole + | Flow::GetRoleFromToken + | Flow::UpdateUserRole + | Flow::GetAuthorizationInfo => Self::UserRole, Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { Self::ConnectorOnboarding diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index c96e099ab163..fe305942d034 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -7,7 +7,7 @@ use crate::{ core::{api_locking, user_role as user_role_core}, services::{ api, - authentication::{self as auth}, + authentication::{self as auth, UserFromToken}, authorization::permissions::Permission, }, }; @@ -64,6 +64,20 @@ pub async fn get_role( .await } +pub async fn get_role_from_token(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::GetRoleFromToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user: UserFromToken, _| user_role_core::get_role_from_token(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn update_user_role( state: web::Data, req: HttpRequest, diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index cef93f82739d..99e4f1b6c096 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -15,16 +15,13 @@ pub struct PermissionInfo { impl PermissionInfo { pub fn new(permissions: &[Permission]) -> Vec { - let mut permission_infos = Vec::with_capacity(permissions.len()); - for permission in permissions { - if let Some(description) = Permission::get_permission_description(permission) { - permission_infos.push(Self { - enum_name: permission.clone(), - description, - }) - } - } - permission_infos + permissions + .iter() + .map(|&per| Self { + description: Permission::get_permission_description(&per), + enum_name: per, + }) + .collect() } } @@ -43,6 +40,7 @@ pub enum PermissionModule { Files, ThreeDsDecisionManager, SurchargeDecisionManager, + AccountCreate, } impl PermissionModule { @@ -60,7 +58,8 @@ impl PermissionModule { Self::Disputes => "Everything related to disputes - like creating and viewing dispute related information are within this module", Self::Files => "Permissions for uploading, deleting and viewing files for disputes", Self::ThreeDsDecisionManager => "View and configure 3DS decision rules configured for a merchant", - Self::SurchargeDecisionManager =>"View and configure surcharge decision rules configured for a merchant" + Self::SurchargeDecisionManager =>"View and configure surcharge decision rules configured for a merchant", + Self::AccountCreate => "Create new account within your organization" } } } @@ -173,6 +172,11 @@ impl ModuleInfo { Permission::SurchargeDecisionManagerRead, ]), }, + PermissionModule::AccountCreate => Self { + module: module_name, + description, + permissions: PermissionInfo::new(&[Permission::MerchantAccountCreate]), + }, } } } diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 426b048e88b7..5c5e3ecce300 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -1,6 +1,6 @@ use strum::Display; -#[derive(PartialEq, Display, Clone, Debug)] +#[derive(PartialEq, Display, Clone, Debug, Copy)] pub enum Permission { PaymentRead, PaymentWrite, @@ -34,45 +34,43 @@ pub enum Permission { } impl Permission { - pub fn get_permission_description(&self) -> Option<&'static str> { + pub fn get_permission_description(&self) -> &'static str { match self { - Self::PaymentRead => Some("View all payments"), - Self::PaymentWrite => Some("Create payment, download payments data"), - Self::RefundRead => Some("View all refunds"), - Self::RefundWrite => Some("Create refund, download refunds data"), - Self::ApiKeyRead => Some("View API keys (masked generated for the system"), - Self::ApiKeyWrite => Some("Create and update API keys"), - Self::MerchantAccountRead => Some("View merchant account details"), + Self::PaymentRead => "View all payments", + Self::PaymentWrite => "Create payment, download payments data", + Self::RefundRead => "View all refunds", + Self::RefundWrite => "Create refund, download refunds data", + Self::ApiKeyRead => "View API keys (masked generated for the system", + Self::ApiKeyWrite => "Create and update API keys", + Self::MerchantAccountRead => "View merchant account details", Self::MerchantAccountWrite => { - Some("Update merchant account details, configure webhooks, manage api keys") + "Update merchant account details, configure webhooks, manage api keys" } - Self::MerchantConnectorAccountRead => Some("View connectors configured"), + Self::MerchantConnectorAccountRead => "View connectors configured", Self::MerchantConnectorAccountWrite => { - Some("Create, update, verify and delete connector configurations") + "Create, update, verify and delete connector configurations" } - Self::ForexRead => Some("Query Forex data"), - Self::RoutingRead => Some("View routing configuration"), - Self::RoutingWrite => Some("Create and activate routing configurations"), - Self::DisputeRead => Some("View disputes"), - Self::DisputeWrite => Some("Create and update disputes"), - Self::MandateRead => Some("View mandates"), - Self::MandateWrite => Some("Create and update mandates"), - Self::CustomerRead => Some("View customers"), - Self::CustomerWrite => Some("Create, update and delete customers"), - Self::FileRead => Some("View files"), - Self::FileWrite => Some("Create, update and delete files"), - Self::Analytics => Some("Access to analytics module"), - Self::ThreeDsDecisionManagerWrite => Some("Create and update 3DS decision rules"), + Self::ForexRead => "Query Forex data", + Self::RoutingRead => "View routing configuration", + Self::RoutingWrite => "Create and activate routing configurations", + Self::DisputeRead => "View disputes", + Self::DisputeWrite => "Create and update disputes", + Self::MandateRead => "View mandates", + Self::MandateWrite => "Create and update mandates", + Self::CustomerRead => "View customers", + Self::CustomerWrite => "Create, update and delete customers", + Self::FileRead => "View files", + Self::FileWrite => "Create, update and delete files", + Self::Analytics => "Access to analytics module", + Self::ThreeDsDecisionManagerWrite => "Create and update 3DS decision rules", Self::ThreeDsDecisionManagerRead => { - Some("View all 3DS decision rules configured for a merchant") + "View all 3DS decision rules configured for a merchant" } - Self::SurchargeDecisionManagerWrite => { - Some("Create and update the surcharge decision rules") - } - Self::SurchargeDecisionManagerRead => Some("View all the surcharge decision rules"), - Self::UsersRead => Some("View all the users for a merchant"), - Self::UsersWrite => Some("Invite users, assign and update roles"), - Self::MerchantAccountCreate => None, + Self::SurchargeDecisionManagerWrite => "Create and update the surcharge decision rules", + Self::SurchargeDecisionManagerRead => "View all the surcharge decision rules", + Self::UsersRead => "View all the users for a merchant", + Self::UsersWrite => "Invite users, assign and update roles", + Self::MerchantAccountCreate => "Create merchant account", } } } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index d271ed5e29d1..53c88f8aea12 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -762,19 +762,13 @@ impl UserFromStorage { } } -impl TryFrom for user_role_api::ModuleInfo { - type Error = (); - fn try_from(value: info::ModuleInfo) -> Result { - let mut permissions = Vec::with_capacity(value.permissions.len()); - for permission in value.permissions { - let permission = permission.try_into()?; - permissions.push(permission); - } - Ok(Self { +impl From for user_role_api::ModuleInfo { + fn from(value: info::ModuleInfo) -> Self { + Self { module: value.module.into(), description: value.description, - permissions, - }) + permissions: value.permissions.into_iter().map(Into::into).collect(), + } } } @@ -794,18 +788,17 @@ impl From for user_role_api::PermissionModule { info::PermissionModule::Files => Self::Files, info::PermissionModule::ThreeDsDecisionManager => Self::ThreeDsDecisionManager, info::PermissionModule::SurchargeDecisionManager => Self::SurchargeDecisionManager, + info::PermissionModule::AccountCreate => Self::AccountCreate, } } } -impl TryFrom for user_role_api::PermissionInfo { - type Error = (); - fn try_from(value: info::PermissionInfo) -> Result { - let enum_name = (&value.enum_name).try_into()?; - Ok(Self { - enum_name, +impl From for user_role_api::PermissionInfo { + fn from(value: info::PermissionInfo) -> Self { + Self { + enum_name: value.enum_name.into(), description: value.description, - }) + } } } diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index c474a82981b1..65ead92ad347 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -1,7 +1,6 @@ use api_models::user_role as user_role_api; use diesel_models::enums::UserStatus; use error_stack::ResultExt; -use router_env::logger; use crate::{ consts, @@ -44,52 +43,50 @@ pub fn validate_role_id(role_id: &str) -> UserResult<()> { pub fn get_role_name_and_permission_response( role_info: &RoleInfo, ) -> Option<(Vec, &'static str)> { - role_info - .get_permissions() - .iter() - .map(TryInto::try_into) - .collect::, _>>() - .ok() - .zip(role_info.get_name()) + role_info.get_name().map(|name| { + ( + role_info + .get_permissions() + .iter() + .map(|&per| per.into()) + .collect::>(), + name, + ) + }) } -impl TryFrom<&Permission> for user_role_api::Permission { - type Error = (); - fn try_from(value: &Permission) -> Result { +impl From for user_role_api::Permission { + fn from(value: Permission) -> Self { match value { - Permission::PaymentRead => Ok(Self::PaymentRead), - Permission::PaymentWrite => Ok(Self::PaymentWrite), - Permission::RefundRead => Ok(Self::RefundRead), - Permission::RefundWrite => Ok(Self::RefundWrite), - Permission::ApiKeyRead => Ok(Self::ApiKeyRead), - Permission::ApiKeyWrite => Ok(Self::ApiKeyWrite), - Permission::MerchantAccountRead => Ok(Self::MerchantAccountRead), - Permission::MerchantAccountWrite => Ok(Self::MerchantAccountWrite), - Permission::MerchantConnectorAccountRead => Ok(Self::MerchantConnectorAccountRead), - Permission::MerchantConnectorAccountWrite => Ok(Self::MerchantConnectorAccountWrite), - Permission::ForexRead => Ok(Self::ForexRead), - Permission::RoutingRead => Ok(Self::RoutingRead), - Permission::RoutingWrite => Ok(Self::RoutingWrite), - Permission::DisputeRead => Ok(Self::DisputeRead), - Permission::DisputeWrite => Ok(Self::DisputeWrite), - Permission::MandateRead => Ok(Self::MandateRead), - Permission::MandateWrite => Ok(Self::MandateWrite), - Permission::CustomerRead => Ok(Self::CustomerRead), - Permission::CustomerWrite => Ok(Self::CustomerWrite), - Permission::FileRead => Ok(Self::FileRead), - Permission::FileWrite => Ok(Self::FileWrite), - Permission::Analytics => Ok(Self::Analytics), - Permission::ThreeDsDecisionManagerWrite => Ok(Self::ThreeDsDecisionManagerWrite), - Permission::ThreeDsDecisionManagerRead => Ok(Self::ThreeDsDecisionManagerRead), - Permission::SurchargeDecisionManagerWrite => Ok(Self::SurchargeDecisionManagerWrite), - Permission::SurchargeDecisionManagerRead => Ok(Self::SurchargeDecisionManagerRead), - Permission::UsersRead => Ok(Self::UsersRead), - Permission::UsersWrite => Ok(Self::UsersWrite), - - Permission::MerchantAccountCreate => { - logger::error!("Invalid use of internal permission"); - Err(()) - } + Permission::PaymentRead => Self::PaymentRead, + Permission::PaymentWrite => Self::PaymentWrite, + Permission::RefundRead => Self::RefundRead, + Permission::RefundWrite => Self::RefundWrite, + Permission::ApiKeyRead => Self::ApiKeyRead, + Permission::ApiKeyWrite => Self::ApiKeyWrite, + Permission::MerchantAccountRead => Self::MerchantAccountRead, + Permission::MerchantAccountWrite => Self::MerchantAccountWrite, + Permission::MerchantConnectorAccountRead => Self::MerchantConnectorAccountRead, + Permission::MerchantConnectorAccountWrite => Self::MerchantConnectorAccountWrite, + Permission::ForexRead => Self::ForexRead, + Permission::RoutingRead => Self::RoutingRead, + Permission::RoutingWrite => Self::RoutingWrite, + Permission::DisputeRead => Self::DisputeRead, + Permission::DisputeWrite => Self::DisputeWrite, + Permission::MandateRead => Self::MandateRead, + Permission::MandateWrite => Self::MandateWrite, + Permission::CustomerRead => Self::CustomerRead, + Permission::CustomerWrite => Self::CustomerWrite, + Permission::FileRead => Self::FileRead, + Permission::FileWrite => Self::FileWrite, + Permission::Analytics => Self::Analytics, + Permission::ThreeDsDecisionManagerWrite => Self::ThreeDsDecisionManagerWrite, + Permission::ThreeDsDecisionManagerRead => Self::ThreeDsDecisionManagerRead, + Permission::SurchargeDecisionManagerWrite => Self::SurchargeDecisionManagerWrite, + Permission::SurchargeDecisionManagerRead => Self::SurchargeDecisionManagerRead, + Permission::UsersRead => Self::UsersRead, + Permission::UsersWrite => Self::UsersWrite, + Permission::MerchantAccountCreate => Self::MerchantAccountCreate, } } } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index c4e0aa3f3ea2..7e3a692517f1 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -297,6 +297,8 @@ pub enum Flow { ListRoles, /// Get role GetRole, + /// Get role from token + GetRoleFromToken, /// Update user role UpdateUserRole, /// Create merchant account for user in a org From 5a791aaf4dc05e8ffdb60464a03b6fc41f860581 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Thu, 18 Jan 2024 22:36:40 +0530 Subject: [PATCH 355/443] refactor(recon): update recipient email and mail body for ProFeatureRequest (#3381) --- crates/router/src/routes/recon.rs | 6 +++--- crates/router/src/services/email/types.rs | 24 +++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/router/src/routes/recon.rs b/crates/router/src/routes/recon.rs index d34e30237ddc..22c886e13581 100644 --- a/crates/router/src/routes/recon.rs +++ b/crates/router/src/routes/recon.rs @@ -102,9 +102,9 @@ pub async fn send_recon_request( user_name: UserName::new(user_from_db.name) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to form username")?, - recipient_email: UserEmail::from_pii_email(user_from_db.email.clone()) + recipient_email: UserEmail::new(Secret::new("biz@hyperswitch.io".to_string())) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert to UserEmail from pii::Email")?, + .attach_printable("Failed to convert recipient's email to UserEmail")?, settings: state.conf.clone(), subject: format!( "Dashboard Pro Feature Request by {}", @@ -187,7 +187,7 @@ pub async fn recon_merchant_account_update( let email_contents = email_types::ReconActivation { recipient_email: UserEmail::from_pii_email(user_email.clone()) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert to UserEmail from pii::Email")?, + .attach_printable("Failed to convert recipient's email to UserEmail from pii::Email")?, user_name: UserName::new(Secret::new("HyperSwitch User".to_string())) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to form username")?, diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 0ef15eaa40d2..d5aa9926130e 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -74,19 +74,17 @@ pub mod html { merchant_id, user_name, user_email, - } => { - format!( - "Dear Hyperswitch Support Team, - - Dashboard Pro Feature Request, - Feature name : {feature_name} - Merchant ID : {merchant_id} - Merchant Name : {user_name} - Email : {user_email} - - (note: This is an auto generated email. use merchant email for any further comunications)", - ) - } + } => format!( + "Dear Hyperswitch Support Team, + +Dashboard Pro Feature Request, +Feature name : {feature_name} +Merchant ID : {merchant_id} +Merchant Name : {user_name} +Email : {user_email} + +(note: This is an auto generated email. Use merchant email for any further communications)", + ), } } } From 8e36fe7348eeb3ceaf31395b2993b3893d4202c3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 00:20:18 +0000 Subject: [PATCH 356/443] chore(version): 2024.01.19.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc63ebaf975..abb03c28492e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.19.0 + +### Features + +- **users:** + - Add `preferred_merchant_id` column and update user details API ([#3373](https://github.com/juspay/hyperswitch/pull/3373)) ([`862a1b5`](https://github.com/juspay/hyperswitch/commit/862a1b5303ff304cca41d3553f652fd1091aab9b)) + - Added get role from jwt api ([#3385](https://github.com/juspay/hyperswitch/pull/3385)) ([`7516a16`](https://github.com/juspay/hyperswitch/commit/7516a16763877c03ecc35fda19388bbd021c5cc7)) + +### Refactors + +- **recon:** Update recipient email and mail body for ProFeatureRequest ([#3381](https://github.com/juspay/hyperswitch/pull/3381)) ([`5a791aa`](https://github.com/juspay/hyperswitch/commit/5a791aaf4dc05e8ffdb60464a03b6fc41f860581)) + +**Full Changelog:** [`2024.01.18.1...2024.01.19.0`](https://github.com/juspay/hyperswitch/compare/2024.01.18.1...2024.01.19.0) + +- - - + ## 2024.01.18.1 ### Bug Fixes From 7a3d8d08423ce2ec6377d2277e727ed12ce4ccd8 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:09:57 +0530 Subject: [PATCH 357/443] ci(pr-convention-checks): add job to check for linked issues for pull requests (#3376) --- .../workflows/conventional-commit-check.yml | 86 ------------ .github/workflows/pr-convention-checks.yml | 128 ++++++++++++++++++ 2 files changed, 128 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/conventional-commit-check.yml create mode 100644 .github/workflows/pr-convention-checks.yml diff --git a/.github/workflows/conventional-commit-check.yml b/.github/workflows/conventional-commit-check.yml deleted file mode 100644 index ad01642068b5..000000000000 --- a/.github/workflows/conventional-commit-check.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Conventional Commit Message Check - -on: - # This is a dangerous event trigger as it causes the workflow to run in the - # context of the target repository. - # Avoid checking out the head of the pull request or building code from the - # pull request whenever this trigger is used. - # Since we only label pull requests, do not have a checkout step in this - # workflow, and restrict permissions on the token, this is an acceptable - # use of this trigger. - pull_request_target: - types: - - opened - - edited - - reopened - - ready_for_review - - synchronize - - merge_group: - types: - - checks_requested - -permissions: - # Reference: https://github.com/cli/cli/issues/6274 - repository-projects: read - pull-requests: write - -env: - # Allow more retries for network requests in cargo (downloading crates) and - # rustup (installing toolchains). This should help to reduce flaky CI failures - # from transient network timeouts or other issues. - CARGO_NET_RETRY: 10 - RUSTUP_MAX_RETRIES: 10 - # Use cargo's sparse index protocol - CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse - -jobs: - pr_title_check: - name: Verify PR title follows conventional commit standards - runs-on: ubuntu-latest - - steps: - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable 2 weeks ago - - - uses: baptiste0928/cargo-install@v2.2.0 - with: - crate: cocogitto - - - name: Verify PR title follows conventional commit standards - id: pr_title_check - if: ${{ github.event_name == 'pull_request_target' }} - shell: bash - env: - TITLE: ${{ github.event.pull_request.title }} - continue-on-error: true - run: cog verify "$TITLE" - - - name: Verify commit message follows conventional commit standards - id: commit_message_check - if: ${{ github.event_name == 'merge_group' }} - shell: bash - # Fail on error, we don't have context about PR information to update labels - continue-on-error: false - run: cog verify '${{ github.event.merge_group.head_commit.message }}' - - # GitHub CLI returns a successful error code even if the PR has the label already attached - - name: Attach 'S-conventions-not-followed' label if PR title check failed - if: ${{ github.event_name == 'pull_request_target' && steps.pr_title_check.outcome == 'failure' }} - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - gh --repo ${{ github.event.repository.full_name }} pr edit --add-label 'S-conventions-not-followed' ${{ github.event.pull_request.number }} - echo "::error::PR title does not follow conventional commit standards" - exit 1 - - # GitHub CLI returns a successful error code even if the PR does not have the label attached - - name: Remove 'S-conventions-not-followed' label if PR title check succeeded - if: ${{ github.event_name == 'pull_request_target' && steps.pr_title_check.outcome == 'success' }} - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: gh --repo ${{ github.event.repository.full_name }} pr edit --remove-label 'S-conventions-not-followed' ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-convention-checks.yml b/.github/workflows/pr-convention-checks.yml new file mode 100644 index 000000000000..37732e7c548c --- /dev/null +++ b/.github/workflows/pr-convention-checks.yml @@ -0,0 +1,128 @@ +name: Pull Request Convention Checks + +on: + # This is a dangerous event trigger as it causes the workflow to run in the + # context of the target repository. + # Avoid checking out the head of the pull request or building code from the + # pull request whenever this trigger is used. + # Since we do not have a checkout step in this workflow, this is an + # acceptable use of this trigger. + pull_request_target: + types: + - opened + - edited + - reopened + - ready_for_review + - synchronize + + merge_group: + types: + - checks_requested + +env: + # Allow more retries for network requests in cargo (downloading crates) and + # rustup (installing toolchains). This should help to reduce flaky CI failures + # from transient network timeouts or other issues. + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + +jobs: + pr_title_conventional_commit_check: + name: Verify PR title follows conventional commit standards + runs-on: ubuntu-latest + + steps: + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - uses: baptiste0928/cargo-install@v2.2.0 + with: + crate: cocogitto + + - name: Verify PR title follows conventional commit standards + if: ${{ github.event_name == 'pull_request_target' }} + shell: bash + env: + TITLE: ${{ github.event.pull_request.title }} + run: cog verify "$TITLE" + + - name: Verify commit message follows conventional commit standards + if: ${{ github.event_name == 'merge_group' }} + shell: bash + run: cog verify '${{ github.event.merge_group.head_commit.message }}' + + pr_linked_issues_check: + name: Verify PR contains one or more linked issues + runs-on: ubuntu-latest + + steps: + - name: Skip check for merge queue + if: ${{ github.event_name == 'merge_group' }} + shell: bash + run: echo "Skipping PR linked issues check for merge queue" + + - name: Generate GitHub app token + id: generate_app_token + if: ${{ github.event_name == 'pull_request_target' }} + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }} + private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }} + owner: ${{ github.event.repository.owner.login }} + + - name: Verify PR contains one or more linked issues + if: ${{ github.event_name == 'pull_request_target' }} + shell: bash + env: + GH_TOKEN: ${{ steps.generate_app_token.outputs.token }} + run: | + # GitHub does not provide information about linked issues for a pull request via the REST API. + # This information is available only within the GraphQL API. + + # Obtain issue number and repository name with owner (in the `owner/repo` format) for all linked issues + query='query ($owner: String!, $repository: String!, $prNumber: Int!) { + repository(owner: $owner, name: $repository) { + pullRequest(number: $prNumber) { + closingIssuesReferences(first: 10) { + nodes { + number + repository { + nameWithOwner + } + } + } + } + } + }' + + # Obtain linked issues in the `owner/repo#issue_number` format, one issue per line. + # The variable contains an empty string if the pull request has no linked issues. + linked_issues="$( + gh api graphql \ + --raw-field "query=${query}" \ + --field 'owner=${{ github.event.repository.owner.login }}' \ + --field 'repository=${{ github.event.repository.name }}' \ + --field 'prNumber=${{ github.event.pull_request.number }}' \ + --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[] | "\(.repository.nameWithOwner)#\(.number)"' + )" + + if [[ -z "${linked_issues}" ]]; then + echo "::error::PR does not contain any linked issues" + exit 1 + else + echo "PR contains at least one linked issue" + fi + + while IFS= read -r issue; do + # Split `${issue}` by `#` to obtain repository with owner (in `owner/repository` format) and issue number + IFS='#' read -r repository_with_owner issue_number <<< "${issue}" + issue_state="$(gh issue view --repo "${repository_with_owner}" --json 'state' "${issue_number}" --jq '.state')" + + # Transform `${issue_state}` to lowercase for comparison + if [[ "${issue_state,,}" != 'open' ]]; then + echo "::error::At least one of the linked issues is not open" + exit 1 + fi + done <<< "${linked_issues}" From 5255ba9170c633899cd4c3bbe24a44b429546f15 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:14:33 +0530 Subject: [PATCH 358/443] fix(frm): update FRM manual review flow (#3176) Co-authored-by: Kashif --- crates/router/src/core/fraud_check.rs | 10 +- crates/router/src/core/payments.rs | 36 ++- .../payments/operations/payment_approve.rs | 266 +++------------- .../payments/operations/payment_reject.rs | 21 +- .../payments/operations/payment_response.rs | 301 ++++++++++-------- crates/router/src/core/payments/retry.rs | 5 + crates/router/src/routes/payments.rs | 16 +- 7 files changed, 261 insertions(+), 394 deletions(-) diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index ad3a7638774e..0e3f67c051b8 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -431,6 +431,7 @@ pub async fn pre_payment_frm_core<'a, F>( frm_configs: FrmConfigsObject, customer: &Option, should_continue_transaction: &mut bool, + should_continue_capture: &mut bool, key_store: domain::MerchantKeyStore, ) -> RouterResult> where @@ -466,13 +467,12 @@ where .await?; let frm_fraud_check = frm_data_updated.fraud_check.clone(); payment_data.frm_message = Some(frm_fraud_check.clone()); - if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) - //DontTakeAction - { - *should_continue_transaction = false; + if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) { if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) { + *should_continue_transaction = false; frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction); } else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) { + *should_continue_capture = false; frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview); } } @@ -582,6 +582,7 @@ pub async fn call_frm_before_connector_call<'a, F, Req, Ctx>( frm_info: &mut Option>, customer: &Option, should_continue_transaction: &mut bool, + should_continue_capture: &mut bool, key_store: domain::MerchantKeyStore, ) -> RouterResult> where @@ -615,6 +616,7 @@ where frm_configs, customer, should_continue_transaction, + should_continue_capture, key_store, ) .await?; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 49a9bcf66645..10aa00f5963c 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -181,6 +181,8 @@ where #[allow(unused_variables, unused_mut)] let mut should_continue_transaction: bool = true; #[cfg(feature = "frm")] + let mut should_continue_capture: bool = true; + #[cfg(feature = "frm")] let frm_configs = if state.conf.frm.enabled { frm_core::call_frm_before_connector_call( db, @@ -191,6 +193,7 @@ where &mut frm_info, &customer, &mut should_continue_transaction, + &mut should_continue_capture, key_store.clone(), ) .await? @@ -199,12 +202,25 @@ where }; #[cfg(feature = "frm")] logger::debug!( - "should_cancel_transaction: {:?} {:?} ", + "frm_configs: {:?}\nshould_cancel_transaction: {:?}\nshould_continue_capture: {:?}", frm_configs, - should_continue_transaction + should_continue_transaction, + should_continue_capture, ); if should_continue_transaction { + #[cfg(feature = "frm")] + match ( + should_continue_capture, + payment_data.payment_attempt.capture_method, + ) { + (false, Some(storage_enums::CaptureMethod::Automatic)) + | (false, Some(storage_enums::CaptureMethod::Scheduled)) => { + payment_data.payment_attempt.capture_method = + Some(storage_enums::CaptureMethod::Manual); + } + _ => (), + }; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -233,6 +249,10 @@ where &validate_result, schedule_time, header_payload, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) .await?; let operation = Box::new(PaymentResponse); @@ -284,6 +304,10 @@ where &validate_result, schedule_time, header_payload, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) .await?; @@ -311,6 +335,10 @@ where &customer, &validate_result, schedule_time, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) .await?; }; @@ -996,6 +1024,7 @@ pub async fn call_connector_service( validate_result: &operations::ValidateResult<'_>, schedule_time: Option, header_payload: HeaderPayload, + frm_suggestion: Option, ) -> RouterResult> where F: Send + Clone + Sync, @@ -1172,7 +1201,7 @@ where merchant_account.storage_scheme, updated_customer, key_store, - None, + frm_suggestion, header_payload, ) .await?; @@ -2110,6 +2139,7 @@ pub fn should_call_connector( } "CompleteAuthorize" => true, "PaymentApprove" => true, + "PaymentReject" => true, "PaymentSession" => true, "PaymentIncrementalAuthorization" => matches!( payment_data.payment_intent.status, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index cddbc89acff1..6d3697caabdf 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -2,54 +2,51 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use data_models::mandates::MandateData; -use error_stack::{report, IntoReport, ResultExt}; +use error_stack::{IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ - errors::{self, CustomResult, RouterResult, StorageErrorExt}, + errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, utils as core_utils, }, - db::StorageInterface, routes::AppState, services, types::{ - self, api::{self, PaymentIdTypeExt}, domain, storage::{self, enums as storage_enums}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(operations = "all", flow = "authorize")] +#[operation(operations = "all", flow = "capture")] pub struct PaymentApprove; #[async_trait] impl - GetTracker, api::PaymentsRequest, Ctx> for PaymentApprove + GetTracker, api::PaymentsCaptureRequest, Ctx> for PaymentApprove { #[instrument(skip_all)] async fn get_trackers<'a>( &'a self, state: &'a AppState, payment_id: &api::PaymentIdType, - request: &api::PaymentsRequest, - mandate_type: Option, + _request: &api::PaymentsCaptureRequest, + _mandate_type: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (mut payment_intent, mut payment_attempt, currency, amount); + let (mut payment_intent, payment_attempt, currency, amount); let payment_id = payment_id .get_payment_intent_id() @@ -59,9 +56,6 @@ impl .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_intent.setup_future_usage = request - .setup_future_usage - .or(payment_intent.setup_future_usage); helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, @@ -69,7 +63,7 @@ impl storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, ], - "confirm", + "approve", )?; let profile_id = payment_intent @@ -87,31 +81,6 @@ impl id: profile_id.to_string(), })?; - let ( - token, - payment_method, - payment_method_type, - setup_mandate, - recurring_mandate_payment_data, - mandate_connector, - ) = helpers::get_token_pm_type_mandate_details( - state, - request, - mandate_type.clone(), - merchant_account, - key_store, - ) - .await?; - - let browser_info = request - .browser_info - .clone() - .map(|x| utils::Encode::::encode_to_value(&x)) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "browser_info", - })?; - let attempt_id = payment_intent.active_attempt.get_id().clone(); payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -123,35 +92,12 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let token = token.or_else(|| payment_attempt.payment_token.clone()); - - helpers::validate_pm_or_token_given( - &request.payment_method, - &request.payment_method_data, - &request.payment_method_type, - &mandate_type, - &token, - )?; - - payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); - payment_attempt.browser_info = browser_info; - payment_attempt.payment_method_type = - payment_method_type.or(payment_attempt.payment_method_type); - payment_attempt.payment_experience = request.payment_experience; currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.get_total_amount().into(); - helpers::validate_customer_id_mandatory_cases( - request.setup_future_usage.is_some(), - &payment_intent - .customer_id - .clone() - .or_else(|| request.customer_id.clone()), - )?; - let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, - request.shipping.as_ref(), + None, payment_intent.shipping_address_id.as_deref(), merchant_id, payment_intent.customer_id.as_ref(), @@ -162,7 +108,7 @@ impl .await?; let billing_address = helpers::create_or_find_address_for_payment_by_request( db, - request.billing.as_ref(), + None, payment_intent.billing_address_id.as_deref(), merchant_id, payment_intent.customer_id.as_ref(), @@ -172,47 +118,8 @@ impl ) .await?; - let redirect_response = request - .feature_metadata - .as_ref() - .and_then(|fm| fm.redirect_response.clone()); - payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request - .return_url - .as_ref() - .map(|a| a.to_string()) - .or(payment_intent.return_url); - - payment_intent.allowed_payment_method_types = request - .get_allowed_payment_method_types_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting allowed_payment_types to Value")? - .or(payment_intent.allowed_payment_method_types); - - payment_intent.connector_metadata = request - .get_connector_metadata_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting connector_metadata to Value")? - .or(payment_intent.connector_metadata); - - payment_intent.feature_metadata = request - .get_feature_metadata_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting feature_metadata to Value")? - .or(payment_intent.feature_metadata); - - payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); - - // The operation merges mandate data from both request and payment_attempt - let setup_mandate = setup_mandate.map(|mandate_data| MandateData { - customer_acceptance: mandate_data.customer_acceptance, - mandate_type: payment_attempt - .mandate_details - .clone() - .or(mandate_data.mandate_type), - }); let frm_response = db .find_fraud_check_by_payment_id(payment_intent.payment_id.clone(), merchant_account.merchant_id.clone()) @@ -228,49 +135,41 @@ impl payment_attempt, currency, amount, - email: request.email.clone(), + email: None, mandate_id: None, - mandate_connector, - setup_mandate, - token, + mandate_connector: None, + setup_mandate: None, + token: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + confirm: None, + payment_method_data: None, force_sync: None, refunds: vec![], disputes: vec![], attempts: None, sessions_token: vec![], - card_cvc: request.card_cvc.clone(), + card_cvc: None, creds_identifier: None, pm_token: None, connector_customer_id: None, - recurring_mandate_payment_data, + recurring_mandate_payment_data: None, ephemeral_key: None, multiple_capture_data: None, - redirect_response, + redirect_response: None, surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], - frm_metadata: request.frm_metadata.clone(), + frm_metadata: None, }; - let customer_details = Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }); - let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), - customer_details, + customer_details: None, payment_data, business_profile, }; @@ -279,91 +178,9 @@ impl } } -#[async_trait] -impl Domain - for PaymentApprove -{ - #[instrument(skip_all)] - async fn get_or_create_customer_details<'a>( - &'a self, - db: &dyn StorageInterface, - payment_data: &mut PaymentData, - request: Option, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult< - ( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - Option, - ), - errors::StorageError, - > { - helpers::create_customer_if_not_exist( - Box::new(self), - db, - payment_data, - request, - &key_store.merchant_id, - key_store, - ) - .await - } - - #[instrument(skip_all)] - async fn make_pm_data<'a>( - &'a self, - state: &'a AppState, - payment_data: &mut PaymentData, - _storage_scheme: storage_enums::MerchantStorageScheme, - merchant_key_store: &domain::MerchantKeyStore, - customer: &Option, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - Option, - )> { - let (op, payment_method_data) = helpers::make_pm_data( - Box::new(self), - state, - payment_data, - merchant_key_store, - customer, - ) - .await?; - - utils::when(payment_method_data.is_none(), || { - Err(errors::ApiErrorResponse::PaymentMethodNotFound) - })?; - - Ok((op, payment_method_data)) - } - - #[instrument(skip_all)] - async fn add_task_to_process_tracker<'a>( - &'a self, - _state: &'a AppState, - _payment_attempt: &storage::PaymentAttempt, - _requeue: bool, - _schedule_time: Option, - ) -> CustomResult<(), errors::ApiErrorResponse> { - Ok(()) - } - - async fn get_connector<'a>( - &'a self, - _merchant_account: &domain::MerchantAccount, - state: &AppState, - request: &api::PaymentsRequest, - _payment_intent: &storage::PaymentIntent, - _key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - // Use a new connector in the confirm call or use the same one which was passed when - // creating the payment or if none is passed then use the routing algorithm - helpers::get_connector_default(state, request.routing.clone()).await - } -} - #[async_trait] impl - UpdateTracker, api::PaymentsRequest, Ctx> for PaymentApprove + UpdateTracker, api::PaymentsCaptureRequest, Ctx> for PaymentApprove { #[instrument(skip_all)] async fn update_trackers<'b>( @@ -377,7 +194,7 @@ impl _frm_suggestion: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( - BoxedOperation<'b, F, api::PaymentsRequest, Ctx>, + BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, PaymentData, )> where @@ -401,16 +218,16 @@ impl } } -impl ValidateRequest - for PaymentApprove +impl + ValidateRequest for PaymentApprove { #[instrument(skip_all)] fn validate_request<'a, 'b>( &'b self, - request: &api::PaymentsRequest, + request: &api::PaymentsCaptureRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult<( - BoxedOperation<'b, F, api::PaymentsRequest, Ctx>, + BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, operations::ValidateResult<'a>, )> { let request_merchant_id = request.merchant_id.as_deref(); @@ -420,28 +237,17 @@ impl ValidateRequest - GetTracker, PaymentsRejectRequest, Ctx> for PaymentReject + GetTracker, PaymentsCancelRequest, Ctx> for PaymentReject { #[instrument(skip_all)] async fn get_trackers<'a>( &'a self, state: &'a AppState, payment_id: &api::PaymentIdType, - _request: &PaymentsRejectRequest, + _request: &PaymentsCancelRequest, _mandate_type: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -57,6 +57,7 @@ impl helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ + enums::IntentStatus::Cancelled, enums::IntentStatus::Failed, enums::IntentStatus::Succeeded, enums::IntentStatus::Processing, @@ -176,7 +177,7 @@ impl #[async_trait] impl - UpdateTracker, PaymentsRejectRequest, Ctx> for PaymentReject + UpdateTracker, PaymentsCancelRequest, Ctx> for PaymentReject { #[instrument(skip_all)] async fn update_trackers<'b>( @@ -190,7 +191,7 @@ impl _should_decline_transaction: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( - BoxedOperation<'b, F, PaymentsRejectRequest, Ctx>, + BoxedOperation<'b, F, PaymentsCancelRequest, Ctx>, PaymentData, )> where @@ -242,16 +243,16 @@ impl } } -impl ValidateRequest +impl ValidateRequest for PaymentReject { #[instrument(skip_all)] fn validate_request<'a, 'b>( &'b self, - request: &PaymentsRejectRequest, + request: &PaymentsCancelRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult<( - BoxedOperation<'b, F, PaymentsRejectRequest, Ctx>, + BoxedOperation<'b, F, PaymentsCancelRequest, Ctx>, operations::ValidateResult<'a>, )> { Ok(( diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 9ab0b4f817f5..e5552f0d156d 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -512,113 +512,132 @@ async fn payment_response_update_tracker( }; (capture_update, attempt_update) } - Ok(payments_response) => match payments_response { - types::PaymentsResponseData::PreProcessingResponse { - pre_processing_id, - connector_metadata, - connector_response_reference_id, - .. - } => { - let connector_transaction_id = match pre_processing_id.to_owned() { - types::PreprocessingResponseId::PreProcessingId(_) => None, - types::PreprocessingResponseId::ConnectorTransactionId(connector_txn_id) => { - Some(connector_txn_id) - } - }; - let preprocessing_step_id = match pre_processing_id { - types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => { - Some(pre_processing_id) + Ok(payments_response) => { + let attempt_status = payment_data.payment_attempt.status.to_owned(); + let connector_status = router_data.status.to_owned(); + let updated_attempt_status = match ( + connector_status, + attempt_status, + payment_data.frm_message.to_owned(), + ) { + ( + enums::AttemptStatus::Authorized, + enums::AttemptStatus::Unresolved, + Some(frm_message), + ) => match frm_message.frm_status { + enums::FraudCheckStatus::Fraud | enums::FraudCheckStatus::ManualReview => { + attempt_status } - types::PreprocessingResponseId::ConnectorTransactionId(_) => None, - }; - let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { - status: router_data.get_attempt_status_for_db_update(&payment_data), - payment_method_id: Some(router_data.payment_method_id), + _ => router_data.get_attempt_status_for_db_update(&payment_data), + }, + _ => router_data.get_attempt_status_for_db_update(&payment_data), + }; + match payments_response { + types::PaymentsResponseData::PreProcessingResponse { + pre_processing_id, connector_metadata, - preprocessing_step_id, - connector_transaction_id, connector_response_reference_id, - updated_by: storage_scheme.to_string(), - }; - - (None, Some(payment_attempt_update)) - } - types::PaymentsResponseData::TransactionResponse { - resource_id, - redirection_data, - connector_metadata, - connector_response_reference_id, - incremental_authorization_allowed, - .. - } => { - payment_data - .payment_intent - .incremental_authorization_allowed = - core_utils::get_incremental_authorization_allowed_value( - incremental_authorization_allowed, - payment_data - .payment_intent - .request_incremental_authorization, - ); - let connector_transaction_id = match resource_id { - types::ResponseId::NoResponseId => None, - types::ResponseId::ConnectorTransactionId(id) - | types::ResponseId::EncodedData(id) => Some(id), - }; - - let encoded_data = payment_data.payment_attempt.encoded_data.clone(); - - let authentication_data = redirection_data - .map(|data| utils::Encode::::encode_to_value(&data)) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not parse the connector response")?; - - // incase of success, update error code and error message - let error_status = if router_data.status == enums::AttemptStatus::Charged { - Some(None) - } else { - None - }; + .. + } => { + let connector_transaction_id = match pre_processing_id.to_owned() { + types::PreprocessingResponseId::PreProcessingId(_) => None, + types::PreprocessingResponseId::ConnectorTransactionId( + connector_txn_id, + ) => Some(connector_txn_id), + }; + let preprocessing_step_id = match pre_processing_id { + types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => { + Some(pre_processing_id) + } + types::PreprocessingResponseId::ConnectorTransactionId(_) => None, + }; + let payment_attempt_update = + storage::PaymentAttemptUpdate::PreprocessingUpdate { + status: updated_attempt_status, + payment_method_id: Some(router_data.payment_method_id), + connector_metadata, + preprocessing_step_id, + connector_transaction_id, + connector_response_reference_id, + updated_by: storage_scheme.to_string(), + }; - if router_data.status == enums::AttemptStatus::Charged { - metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); + (None, Some(payment_attempt_update)) } + types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + connector_metadata, + connector_response_reference_id, + incremental_authorization_allowed, + .. + } => { + payment_data + .payment_intent + .incremental_authorization_allowed = + core_utils::get_incremental_authorization_allowed_value( + incremental_authorization_allowed, + payment_data + .payment_intent + .request_incremental_authorization, + ); + let connector_transaction_id = match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }; - utils::add_apple_pay_payment_status_metrics( - router_data.status, - router_data.apple_pay_flow.clone(), - payment_data.payment_attempt.connector.clone(), - payment_data.payment_attempt.merchant_id.clone(), - ); + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); - let (capture_updates, payment_attempt_update) = match payment_data - .multiple_capture_data - { - Some(multiple_capture_data) => { - let capture_update = storage::CaptureUpdate::ResponseUpdate { - status: enums::CaptureStatus::foreign_try_from(router_data.status)?, - connector_capture_id: connector_transaction_id.clone(), - connector_response_reference_id, - }; - let capture_update_list = vec![( - multiple_capture_data.get_latest_capture().clone(), - capture_update, - )]; - (Some((multiple_capture_data, capture_update_list)), None) + let authentication_data = redirection_data + .map(|data| utils::Encode::::encode_to_value(&data)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")?; + + // incase of success, update error code and error message + let error_status = if router_data.status == enums::AttemptStatus::Charged { + Some(None) + } else { + None + }; + + if router_data.status == enums::AttemptStatus::Charged { + metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); } - None => { - let status = router_data.get_attempt_status_for_db_update(&payment_data); - ( + + utils::add_apple_pay_payment_status_metrics( + router_data.status, + router_data.apple_pay_flow.clone(), + payment_data.payment_attempt.connector.clone(), + payment_data.payment_attempt.merchant_id.clone(), + ); + + let (capture_updates, payment_attempt_update) = match payment_data + .multiple_capture_data + { + Some(multiple_capture_data) => { + let capture_update = storage::CaptureUpdate::ResponseUpdate { + status: enums::CaptureStatus::foreign_try_from(router_data.status)?, + connector_capture_id: connector_transaction_id.clone(), + connector_response_reference_id, + }; + let capture_update_list = vec![( + multiple_capture_data.get_latest_capture().clone(), + capture_update, + )]; + (Some((multiple_capture_data, capture_update_list)), None) + } + None => ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status, + status: updated_attempt_status, connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, amount_capturable: router_data .request - .get_amount_capturable(&payment_data, status), + .get_amount_capturable(&payment_data, updated_attempt_status), payment_method_id: Some(router_data.payment_method_id), mandate_id: payment_data .mandate_id @@ -636,56 +655,58 @@ async fn payment_response_update_tracker( authentication_data, encoded_data, }), - ) - } - }; + ), + }; - (capture_updates, payment_attempt_update) - } - types::PaymentsResponseData::TransactionUnresolvedResponse { - resource_id, - reason, - connector_response_reference_id, - } => { - let connector_transaction_id = match resource_id { - types::ResponseId::NoResponseId => None, - types::ResponseId::ConnectorTransactionId(id) - | types::ResponseId::EncodedData(id) => Some(id), - }; - ( - None, - Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { - status: router_data.get_attempt_status_for_db_update(&payment_data), - connector: None, - connector_transaction_id, - payment_method_id: Some(router_data.payment_method_id), - error_code: Some(reason.clone().map(|cd| cd.code)), - error_message: Some(reason.clone().map(|cd| cd.message)), - error_reason: Some(reason.map(|cd| cd.message)), - connector_response_reference_id, - updated_by: storage_scheme.to_string(), - }), - ) - } - types::PaymentsResponseData::SessionResponse { .. } => (None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), - types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), - types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), - types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => (None, None), - types::PaymentsResponseData::MultipleCaptureResponse { - capture_sync_response_list, - } => match payment_data.multiple_capture_data { - Some(multiple_capture_data) => { - let capture_update_list = response_to_capture_update( - &multiple_capture_data, - capture_sync_response_list, - )?; - (Some((multiple_capture_data, capture_update_list)), None) + (capture_updates, payment_attempt_update) } - None => (None, None), - }, - }, + types::PaymentsResponseData::TransactionUnresolvedResponse { + resource_id, + reason, + connector_response_reference_id, + } => { + let connector_transaction_id = match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }; + ( + None, + Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { + status: updated_attempt_status, + connector: None, + connector_transaction_id, + payment_method_id: Some(router_data.payment_method_id), + error_code: Some(reason.clone().map(|cd| cd.code)), + error_message: Some(reason.clone().map(|cd| cd.message)), + error_reason: Some(reason.map(|cd| cd.message)), + connector_response_reference_id, + updated_by: storage_scheme.to_string(), + }), + ) + } + types::PaymentsResponseData::SessionResponse { .. } => (None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), + types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), + types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), + types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => { + (None, None) + } + types::PaymentsResponseData::MultipleCaptureResponse { + capture_sync_response_list, + } => match payment_data.multiple_capture_data { + Some(multiple_capture_data) => { + let capture_update_list = response_to_capture_update( + &multiple_capture_data, + capture_sync_response_list, + )?; + (Some((multiple_capture_data, capture_update_list)), None) + } + None => (None, None), + }, + } + } }; payment_data.multiple_capture_data = match capture_update { Some((mut multiple_capture_data, capture_updates)) => { diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 0fd45c5af3b5..8d74eb3fa961 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -40,6 +40,7 @@ pub async fn do_gsm_actions( customer: &Option, validate_result: &operations::ValidateResult<'_>, schedule_time: Option, + frm_suggestion: Option, ) -> RouterResult> where F: Clone + Send + Sync, @@ -90,6 +91,7 @@ where validate_result, schedule_time, true, + frm_suggestion, ) .await?; } @@ -133,6 +135,7 @@ where schedule_time, //this is an auto retry payment, but not step-up false, + frm_suggestion, ) .await?; @@ -275,6 +278,7 @@ pub async fn do_retry( validate_result: &operations::ValidateResult<'_>, schedule_time: Option, is_step_up: bool, + frm_suggestion: Option, ) -> RouterResult> where F: Clone + Send + Sync, @@ -310,6 +314,7 @@ where validate_result, schedule_time, api::HeaderPayload::default(), + frm_suggestion, ) .await } diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 34f41c49cddf..379cd4f2f1fc 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -968,7 +968,7 @@ pub async fn payments_approve( payload.clone(), |state, auth, req| { payments::payments_core::< - api_types::Authorize, + api_types::Capture, payment_types::PaymentsResponse, _, _, @@ -979,10 +979,8 @@ pub async fn payments_approve( auth.merchant_account, auth.key_store, payments::PaymentApprove, - payment_types::PaymentsRequest { - payment_id: Some(payment_types::PaymentIdType::PaymentIntentId( - req.payment_id, - )), + payment_types::PaymentsCaptureRequest { + payment_id: req.payment_id, ..Default::default() }, api::AuthFlow::Merchant, @@ -1030,7 +1028,7 @@ pub async fn payments_reject( payload.clone(), |state, auth, req| { payments::payments_core::< - api_types::Reject, + api_types::Void, payment_types::PaymentsResponse, _, _, @@ -1041,7 +1039,11 @@ pub async fn payments_reject( auth.merchant_account, auth.key_store, payments::PaymentReject, - req, + payment_types::PaymentsCancelRequest { + payment_id: req.payment_id, + cancellation_reason: Some("Rejected by merchant".to_string()), + ..Default::default() + }, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, None, From 1c04ac751240f5c931df0f282af1e0ad745e9509 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:55:48 +0530 Subject: [PATCH 359/443] refactor: rename `s3` feature flag to `aws_s3` (#3341) --- crates/router/Cargo.toml | 4 ++-- crates/router/src/configs/settings.rs | 6 +++--- crates/router/src/configs/validations.rs | 2 +- crates/router/src/core/files.rs | 8 ++++---- crates/router/src/core/files/helpers.rs | 24 ++++++++++++------------ 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 8897fdac2c22..88272033fb04 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -10,12 +10,12 @@ license.workspace = true [features] default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] -s3 = ["dep:aws-sdk-s3", "dep:aws-config"] +aws_s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] +release = ["kms", "stripe", "aws_s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index bcf26d63ae8d..cb4fdd70eb64 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -88,7 +88,7 @@ pub struct Settings { pub api_keys: ApiKeys, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] pub file_upload_config: FileUploadConfig, pub tokenization: TokenizationConfig, pub connector_customer: ConnectorCustomer, @@ -717,7 +717,7 @@ pub struct ApiKeys { pub expiry_reminder_days: Vec, } -#[cfg(feature = "s3")] +#[cfg(feature = "aws_s3")] #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct FileUploadConfig { @@ -849,7 +849,7 @@ impl Settings { self.kms .validate() .map_err(|error| ApplicationError::InvalidConfigurationValueError(error.into()))?; - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] self.file_upload_config.validate()?; self.lock_settings.validate()?; self.events.validate()?; diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 569262d0d210..0b286ece8435 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -127,7 +127,7 @@ impl super::settings::DrainerSettings { } } -#[cfg(feature = "s3")] +#[cfg(feature = "aws_s3")] impl super::settings::FileUploadConfig { pub fn validate(&self) -> Result<(), ApplicationError> { use common_utils::fp_utils::when; diff --git a/crates/router/src/core/files.rs b/crates/router/src/core/files.rs index 13c4d3dfdf31..f3e564898061 100644 --- a/crates/router/src/core/files.rs +++ b/crates/router/src/core/files.rs @@ -1,8 +1,8 @@ pub mod helpers; -#[cfg(feature = "s3")] +#[cfg(feature = "aws_s3")] pub mod s3_utils; -#[cfg(not(feature = "s3"))] +#[cfg(not(feature = "aws_s3"))] pub mod fs_utils; use api_models::files; @@ -29,9 +29,9 @@ pub async fn files_create_core( ) .await?; let file_id = common_utils::generate_id(consts::ID_LENGTH, "file"); - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] let file_key = format!("{}/{}", merchant_account.merchant_id, file_id); - #[cfg(not(feature = "s3"))] + #[cfg(not(feature = "aws_s3"))] let file_key = format!("{}_{}", merchant_account.merchant_id, file_id); let file_new = diesel_models::file::FileMetadataNew { file_id: file_id.clone(), diff --git a/crates/router/src/core/files/helpers.rs b/crates/router/src/core/files/helpers.rs index 818067207f40..9205d42aeee7 100644 --- a/crates/router/src/core/files/helpers.rs +++ b/crates/router/src/core/files/helpers.rs @@ -31,33 +31,33 @@ pub async fn get_file_purpose(field: &mut Field) -> Option { } pub async fn upload_file( - #[cfg(feature = "s3")] state: &AppState, + #[cfg(feature = "aws_s3")] state: &AppState, file_key: String, file: Vec, ) -> CustomResult<(), errors::ApiErrorResponse> { - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] return files::s3_utils::upload_file_to_s3(state, file_key, file).await; - #[cfg(not(feature = "s3"))] + #[cfg(not(feature = "aws_s3"))] return files::fs_utils::save_file_to_fs(file_key, file); } pub async fn delete_file( - #[cfg(feature = "s3")] state: &AppState, + #[cfg(feature = "aws_s3")] state: &AppState, file_key: String, ) -> CustomResult<(), errors::ApiErrorResponse> { - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] return files::s3_utils::delete_file_from_s3(state, file_key).await; - #[cfg(not(feature = "s3"))] + #[cfg(not(feature = "aws_s3"))] return files::fs_utils::delete_file_from_fs(file_key); } pub async fn retrieve_file( - #[cfg(feature = "s3")] state: &AppState, + #[cfg(feature = "aws_s3")] state: &AppState, file_key: String, ) -> CustomResult, errors::ApiErrorResponse> { - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] return files::s3_utils::retrieve_file_from_s3(state, file_key).await; - #[cfg(not(feature = "s3"))] + #[cfg(not(feature = "aws_s3"))] return files::fs_utils::retrieve_file_from_fs(file_key); } @@ -134,7 +134,7 @@ pub async fn delete_file_using_file_id( match provider { diesel_models::enums::FileUploadProvider::Router => { delete_file( - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] state, provider_file_id, ) @@ -235,7 +235,7 @@ pub async fn retrieve_file_and_provider_file_id_from_file_id( diesel_models::enums::FileUploadProvider::Router => Ok(( Some( retrieve_file( - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] state, provider_file_id.clone(), ) @@ -365,7 +365,7 @@ pub async fn upload_and_get_provider_provider_file_id_profile_id( )) } else { upload_file( - #[cfg(feature = "s3")] + #[cfg(feature = "aws_s3")] state, file_key.clone(), create_file_request.file.clone(), From ec16ed0f82f258c5699d54a386f67aff06c0d144 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:01:25 +0530 Subject: [PATCH 360/443] fix(connector): [CRYPTOPAY] Fix header generation for PSYNC (#3402) --- crates/router/src/connector/cryptopay.rs | 26 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index 95ea7ef0c7a9..8727461ba34c 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -69,14 +69,24 @@ where req: &types::RouterData, connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let api_method = self.get_http_method().to_string(); - let body = types::RequestBody::get_inner_value(self.get_request_body(req, connectors)?) - .peek() - .to_owned(); - let md5_payload = crypto::Md5 - .generate_digest(body.as_bytes()) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let payload = encode(md5_payload); + let method = self.get_http_method(); + let payload = match method { + common_utils::request::Method::Get => String::default(), + common_utils::request::Method::Post + | common_utils::request::Method::Put + | common_utils::request::Method::Delete + | common_utils::request::Method::Patch => { + let body = + types::RequestBody::get_inner_value(self.get_request_body(req, connectors)?) + .peek() + .to_owned(); + let md5_payload = crypto::Md5 + .generate_digest(body.as_bytes()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + encode(md5_payload) + } + }; + let api_method = method.to_string(); let now = date_time::date_as_yyyymmddthhmmssmmmz() .into_report() From d134d9c82f299c5b5bbf75a729c1b9194f84b945 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:37:30 +0000 Subject: [PATCH 361/443] chore(version): 2024.01.19.1 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abb03c28492e..d59aac3f7fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.19.1 + +### Bug Fixes + +- **connector:** [CRYPTOPAY] Fix header generation for PSYNC ([#3402](https://github.com/juspay/hyperswitch/pull/3402)) ([`ec16ed0`](https://github.com/juspay/hyperswitch/commit/ec16ed0f82f258c5699d54a386f67aff06c0d144)) +- **frm:** Update FRM manual review flow ([#3176](https://github.com/juspay/hyperswitch/pull/3176)) ([`5255ba9`](https://github.com/juspay/hyperswitch/commit/5255ba9170c633899cd4c3bbe24a44b429546f15)) + +### Refactors + +- Rename `s3` feature flag to `aws_s3` ([#3341](https://github.com/juspay/hyperswitch/pull/3341)) ([`1c04ac7`](https://github.com/juspay/hyperswitch/commit/1c04ac751240f5c931df0f282af1e0ad745e9509)) + +**Full Changelog:** [`2024.01.19.0...2024.01.19.1`](https://github.com/juspay/hyperswitch/compare/2024.01.19.0...2024.01.19.1) + +- - - + ## 2024.01.19.0 ### Features From a47372a451b60defda35fa212565b889ed5b2d2b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 19 Jan 2024 18:35:04 +0530 Subject: [PATCH 362/443] feat(user_roles): Add accept invitation API and `UserJWTAuth` (#3365) --- crates/api_models/src/events/user_role.rs | 7 +-- crates/api_models/src/user_role.rs | 10 ++++ crates/router/src/core/user.rs | 17 +++---- crates/router/src/core/user_role.rs | 48 +++++++++++++++++- crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/user_role.rs | 19 +++++++ crates/router/src/services/authentication.rs | 53 +++++++++++++++++++- crates/router/src/types/domain/user.rs | 2 +- crates/router/src/utils/user.rs | 21 +++++--- crates/router_env/src/logger/types.rs | 2 + 11 files changed, 159 insertions(+), 24 deletions(-) diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index aa8d13dab6df..c8d8fd96a7a6 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -1,8 +1,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ - AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, RoleInfoResponse, - UpdateUserRoleRequest, + AcceptInvitationRequest, AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, + RoleInfoResponse, UpdateUserRoleRequest, }; common_utils::impl_misc_api_event_type!( @@ -10,5 +10,6 @@ common_utils::impl_misc_api_event_type!( RoleInfoResponse, GetRoleRequest, AuthorizationInfoResponse, - UpdateUserRoleRequest + UpdateUserRoleRequest, + AcceptInvitationRequest ); diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index b057f8ca8bce..d2548935f62a 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -1,3 +1,5 @@ +use crate::user::DashboardEntryResponse; + #[derive(Debug, serde::Serialize)] pub struct ListRolesResponse(pub Vec); @@ -91,3 +93,11 @@ pub enum UserStatus { Active, InvitationSent, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct AcceptInvitationRequest { + pub merchant_ids: Vec, + pub need_dashboard_entry_response: Option, +} + +pub type AcceptInvitationResponse = DashboardEntryResponse; diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 729cef65c20a..3384e2290097 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -90,11 +90,10 @@ pub async fn signup( UserStatus::Active, ) .await?; - let token = - utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, )) } @@ -118,11 +117,10 @@ pub async fn signin( user_from_db.compare_password(request.password)?; let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let token = - utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, )) } @@ -600,7 +598,7 @@ pub async fn switch_merchant_id( .ok_or(UserErrors::InvalidRoleOperation.into()) .attach_printable("User doesn't have access to switch")?; - let token = utils::user::generate_jwt_auth_token(state, &user, user_role).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user, user_role).await?; (token, user_role.role_id.clone()) }; @@ -712,11 +710,10 @@ pub async fn verify_email( let user_from_db: domain::UserFromStorage = user.into(); let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let token = - utils::user::generate_jwt_auth_token(state.clone(), &user_from_db, &user_role).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(state, user_from_db, user_role, token)?, + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, )) } diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index d8ff836e1f88..245f8d246d23 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -1,6 +1,7 @@ use api_models::user_role as user_role_api; -use diesel_models::user_role::UserRoleUpdate; +use diesel_models::{enums::UserStatus, user_role::UserRoleUpdate}; use error_stack::ResultExt; +use router_env::logger; use crate::{ core::errors::{UserErrors, UserResponse}, @@ -115,3 +116,48 @@ pub async fn update_user_role( Ok(ApplicationResponse::StatusOk) } + +pub async fn accept_invitation( + state: AppState, + user_token: auth::UserWithoutMerchantFromToken, + req: user_role_api::AcceptInvitationRequest, +) -> UserResponse { + let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async { + state + .store + .update_user_role_by_user_id_merchant_id( + user_token.user_id.as_str(), + merchant_id, + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user_token.user_id.clone(), + }, + ) + .await + .map_err(|e| { + logger::error!("Error while accepting invitation {}", e); + }) + .ok() + })) + .await + .into_iter() + .reduce(Option::or) + .flatten() + .ok_or(UserErrors::MerchantIdNotFound)?; + + if let Some(true) = req.need_dashboard_entry_response { + let user_from_db = state + .store + .find_user_by_id(user_token.user_id.as_str()) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + return Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + )); + } + + Ok(ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 3d63df2fe800..4345109a6724 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -922,6 +922,7 @@ impl User { .service(web::resource("/role").route(web::get().to(get_role_from_token))) .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) .service(web::resource("/user/invite").route(web::post().to(invite_user))) + .service(web::resource("/user/invite/accept").route(web::post().to(accept_invitation))) .service(web::resource("/update").route(web::post().to(update_user_account_details))) .service( web::resource("/data") diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index d3a2e1af9a71..1c967222dc7f 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -185,7 +185,8 @@ impl From for ApiIdentifier { | Flow::GetRole | Flow::GetRoleFromToken | Flow::UpdateUserRole - | Flow::GetAuthorizationInfo => Self::UserRole, + | Flow::GetAuthorizationInfo + | Flow::AcceptInvitation => Self::UserRole, Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { Self::ConnectorOnboarding diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index fe305942d034..73b1ef1b01da 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -96,3 +96,22 @@ pub async fn update_user_role( )) .await } + +pub async fn accept_invitation( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AcceptInvitation; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload, + user_role_core::accept_invitation, + &auth::UserWithoutMerchantJWTAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 3370912394e0..eaadc0d5c7be 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -55,6 +55,9 @@ pub enum AuthenticationType { merchant_id: String, user_id: Option, }, + UserJwt { + user_id: String, + }, MerchantId { merchant_id: String, }, @@ -81,11 +84,32 @@ impl AuthenticationType { user_id: _, } | Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()), - Self::AdminApiKey | Self::NoAuth => None, + Self::AdminApiKey | Self::UserJwt { .. } | Self::NoAuth => None, } } } +#[derive(Clone, Debug)] +pub struct UserWithoutMerchantFromToken { + pub user_id: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct UserAuthToken { + pub user_id: String, + pub exp: u64, +} + +#[cfg(feature = "olap")] +impl UserAuthToken { + pub async fn new_token(user_id: String, settings: &settings::Settings) -> UserResult { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration)?.as_secs(); + let token_payload = Self { user_id, exp }; + jwt::generate_jwt(&token_payload, settings).await + } +} + #[derive(serde::Serialize, serde::Deserialize)] pub struct AuthToken { pub user_id: String, @@ -276,6 +300,33 @@ pub async fn get_admin_api_key( .await } +#[derive(Debug)] +pub struct UserWithoutMerchantJWTAuth; + +#[cfg(feature = "olap")] +#[async_trait] +impl AuthenticateAndFetch for UserWithoutMerchantJWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + Ok(( + UserWithoutMerchantFromToken { + user_id: payload.user_id.clone(), + }, + AuthenticationType::UserJwt { + user_id: payload.user_id, + }, + )) + } +} + #[derive(Debug)] pub struct AdminApiAuth; diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 53c88f8aea12..bbe21f289aa1 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -739,7 +739,7 @@ impl UserFromStorage { } #[cfg(feature = "email")] - pub fn get_verification_days_left(&self, state: AppState) -> UserResult> { + pub fn get_verification_days_left(&self, state: &AppState) -> UserResult> { if self.0.is_verified { return Ok(None); } diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index a115fa2a2d8a..a3f9e7978aa1 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -56,7 +56,7 @@ impl UserFromToken { } pub async fn generate_jwt_auth_token( - state: AppState, + state: &AppState, user: &UserFromStorage, user_role: &UserRole, ) -> UserResult> { @@ -89,17 +89,13 @@ pub async fn generate_jwt_auth_token_with_custom_role_attributes( Ok(Secret::new(token)) } -#[allow(unused_variables)] pub fn get_dashboard_entry_response( - state: AppState, + state: &AppState, user: UserFromStorage, user_role: UserRole, token: Secret, ) -> UserResult { - #[cfg(feature = "email")] - let verification_days_left = user.get_verification_days_left(state)?; - #[cfg(not(feature = "email"))] - let verification_days_left = None; + let verification_days_left = get_verification_days_left(state, &user)?; Ok(user_api::DashboardEntryResponse { merchant_id: user_role.merchant_id, @@ -111,3 +107,14 @@ pub fn get_dashboard_entry_response( user_role: user_role.role_id, }) } + +#[allow(unused_variables)] +pub fn get_verification_days_left( + state: &AppState, + user: &UserFromStorage, +) -> UserResult> { + #[cfg(feature = "email")] + return user.get_verification_days_left(state); + #[cfg(not(feature = "email"))] + return Ok(None); +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7e3a692517f1..ba323ebc5e3f 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -335,6 +335,8 @@ pub enum Flow { VerifyEmailRequest, /// Update user account details UpdateUserAccountDetails, + /// Accept user invitation + AcceptInvitation, } /// From 4e1e78ecd962f4b34fa04f611f03e8e6f6e1bd7c Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Fri, 19 Jan 2024 19:32:34 +0530 Subject: [PATCH 363/443] docs: add link to api docs (#3405) Co-authored-by: venkatesh.devendran --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dfa77ebe0666..0f5e924589f2 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ The single API to access payment ecosystems across 130+ countries Quick Start GuideLocal Setup GuideFast Integration for Stripe Users • + API Docs Supported Features • - FAQs
What's IncludedJoin us in building HyperSwitchCommunityBugs and feature requests • + FAQsVersioningCopyright and License

From 3fba38a06daeccbd7023e508028de9f793ba7713 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:21:11 +0000 Subject: [PATCH 364/443] chore(version): 2024.01.22.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59aac3f7fa1..51d650f3fb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.22.0 + +### Features + +- **user_roles:** Add accept invitation API and `UserJWTAuth` ([#3365](https://github.com/juspay/hyperswitch/pull/3365)) ([`a47372a`](https://github.com/juspay/hyperswitch/commit/a47372a451b60defda35fa212565b889ed5b2d2b)) + +### Documentation + +- Add link to api docs ([#3405](https://github.com/juspay/hyperswitch/pull/3405)) ([`4e1e78e`](https://github.com/juspay/hyperswitch/commit/4e1e78ecd962f4b34fa04f611f03e8e6f6e1bd7c)) + +**Full Changelog:** [`2024.01.19.1...2024.01.22.0`](https://github.com/juspay/hyperswitch/compare/2024.01.19.1...2024.01.22.0) + +- - - + ## 2024.01.19.1 ### Bug Fixes From 6c46e9c19b304bb11f304e60c46e8abf67accf6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:53:09 +0530 Subject: [PATCH 365/443] chore(deps): bump the cargo group across 1 directories with 3 updates (#3409) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 261 +++++++++++++++++----------- crates/analytics/Cargo.toml | 2 +- crates/common_utils/Cargo.toml | 2 +- crates/drainer/Cargo.toml | 2 +- crates/external_services/Cargo.toml | 2 +- crates/redis_interface/Cargo.toml | 4 +- crates/router/Cargo.toml | 4 +- crates/router_env/Cargo.toml | 4 +- crates/scheduler/Cargo.toml | 2 +- crates/storage_impl/Cargo.toml | 2 +- crates/test_utils/Cargo.toml | 2 +- 11 files changed, 171 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ce0851ba159..2cd3cd65c318 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ "futures-sink", "memchr", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", ] @@ -67,7 +67,7 @@ dependencies = [ "rand 0.8.5", "sha1", "smallvec 1.11.1", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", "zstd", @@ -105,7 +105,7 @@ dependencies = [ "serde_json", "serde_plain", "tempfile", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -142,7 +142,7 @@ checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "actix-macros", "futures-core", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -156,9 +156,9 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 0.8.8", - "socket2 0.5.4", - "tokio 1.32.0", + "mio 0.8.10", + "socket2 0.5.5", + "tokio 1.35.1", "tracing", ] @@ -188,7 +188,7 @@ dependencies = [ "pin-project-lite", "rustls 0.21.7", "rustls-webpki", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-rustls", "tokio-util", "tracing", @@ -360,7 +360,7 @@ dependencies = [ "strum 0.25.0", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -508,7 +508,7 @@ dependencies = [ "bb8", "diesel", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", ] @@ -533,7 +533,7 @@ dependencies = [ "futures-core", "memchr", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -631,7 +631,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -658,7 +658,7 @@ dependencies = [ "hyper", "ring", "time", - "tokio 1.32.0", + "tokio 1.35.1", "tower", "tracing", "zeroize", @@ -673,7 +673,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-types", "fastrand 1.9.0", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", "zeroize", ] @@ -914,7 +914,7 @@ checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880" dependencies = [ "futures-util", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", ] @@ -958,7 +958,7 @@ dependencies = [ "lazy_static", "pin-project-lite", "rustls 0.20.9", - "tokio 1.32.0", + "tokio 1.35.1", "tower", "tracing", ] @@ -992,7 +992,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "pin-utils", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", ] @@ -1168,7 +1168,7 @@ dependencies = [ "futures-channel", "futures-util", "parking_lot 0.12.1", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -1531,7 +1531,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1685,7 +1685,7 @@ dependencies = [ "test-case", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -2071,7 +2071,7 @@ dependencies = [ "deadpool-runtime", "num_cpus", "retain_mut", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -2283,7 +2283,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -2324,23 +2324,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -2439,7 +2428,7 @@ dependencies = [ "router_env", "serde", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -2469,7 +2458,7 @@ dependencies = [ "serde", "serde_json", "time", - "tokio 1.32.0", + "tokio 1.35.1", "url", "webdriver", ] @@ -2562,8 +2551,8 @@ dependencies = [ "rand 0.8.5", "redis-protocol", "semver 1.0.19", - "socket2 0.5.4", - "tokio 1.32.0", + "socket2 0.5.5", + "tokio 1.35.1", "tokio-stream", "tokio-util", "tracing", @@ -2789,7 +2778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2877,9 +2866,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes 1.5.0", "fnv", @@ -2887,9 +2876,9 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tracing", ] @@ -3073,7 +3062,7 @@ dependencies = [ "itoa", "pin-project-lite", "socket2 0.4.9", - "tokio 1.32.0", + "tokio 1.35.1", "tower-service", "tracing", "want", @@ -3092,7 +3081,7 @@ dependencies = [ "hyper", "hyper-tls", "native-tls", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", "tower-service", ] @@ -3108,7 +3097,7 @@ dependencies = [ "log", "rustls 0.20.9", "rustls-native-certs", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-rustls", ] @@ -3120,7 +3109,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-io-timeout", ] @@ -3133,7 +3122,7 @@ dependencies = [ "bytes 1.5.0", "hyper", "native-tls", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", ] @@ -3287,7 +3276,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3474,9 +3463,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-channel" @@ -3725,14 +3714,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -4040,7 +4029,7 @@ dependencies = [ "opentelemetry-proto", "prost", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tonic", ] @@ -4091,7 +4080,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", ] @@ -4194,7 +4183,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec 1.11.1", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -4796,7 +4785,7 @@ dependencies = [ "serde_derive", "serde_json", "slab", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -4836,7 +4825,7 @@ dependencies = [ "router_env", "serde", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", ] @@ -4970,7 +4959,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "system-configuration", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", "tokio-util", "tower-service", @@ -5141,7 +5130,7 @@ dependencies = [ "test_utils", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", "tracing-futures", "unicode-segmentation", "url", @@ -5184,7 +5173,7 @@ dependencies = [ "serde_path_to_error", "strum 0.24.1", "time", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", "tracing-actix-web", "tracing-appender", @@ -5315,15 +5304,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -5430,7 +5419,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -5463,7 +5452,7 @@ dependencies = [ "strum 0.24.1", "thiserror", "time", - "tokio 1.32.0", + "tokio 1.35.1", "uuid", ] @@ -5793,7 +5782,7 @@ dependencies = [ "futures-core", "libc", "signal-hook", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -5880,12 +5869,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -5995,7 +5984,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "native-tls", "once_cell", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-native-tls", ] @@ -6030,7 +6019,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -6204,7 +6193,7 @@ dependencies = [ "fastrand 2.0.1", "redox_syscall 0.3.5", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -6280,7 +6269,7 @@ dependencies = [ "serial_test", "thirtyfour", "time", - "tokio 1.32.0", + "tokio 1.35.1", "toml 0.7.4", ] @@ -6305,7 +6294,7 @@ dependencies = [ "stringmatch", "thirtyfour-macros", "thiserror", - "tokio 1.32.0", + "tokio 1.35.1", "url", "urlparse", ] @@ -6441,21 +6430,21 @@ dependencies = [ [[package]] name = "tokio" -version = "1.32.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes 1.5.0", "libc", - "mio 0.8.8", + "mio 0.8.10", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -6518,14 +6507,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -6539,7 +6528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] @@ -6568,7 +6557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", - "tokio 1.32.0", + "tokio 1.35.1", "webpki", ] @@ -6580,7 +6569,7 @@ checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", ] @@ -6680,7 +6669,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project-lite", - "tokio 1.32.0", + "tokio 1.35.1", "tracing", ] @@ -6761,7 +6750,7 @@ dependencies = [ "pin-project", "prost", "prost-derive", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-stream", "tokio-util", "tower", @@ -6784,7 +6773,7 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "slab", - "tokio 1.32.0", + "tokio 1.35.1", "tokio-util", "tower-layer", "tower-service", @@ -7422,7 +7411,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -7431,7 +7420,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -7440,13 +7438,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -7455,42 +7468,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.19" @@ -7507,7 +7562,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if 1.0.0", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -7529,7 +7584,7 @@ dependencies = [ "regex", "serde", "serde_json", - "tokio 1.32.0", + "tokio 1.35.1", ] [[package]] diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml index 25066970ddcd..8e7e38d19d9e 100644 --- a/crates/analytics/Cargo.toml +++ b/crates/analytics/Cargo.toml @@ -34,4 +34,4 @@ sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-ac strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.43" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 739129d02db2..3e6ee40f3a7b 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -37,7 +37,7 @@ signal-hook = { version = "0.3.15", optional = true } strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"], optional = true } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 50e0effd03e0..f26c31f0e72c 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -24,7 +24,7 @@ serde = "1.0.193" serde_json = "1.0.108" serde_path_to_error = "0.1.14" thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } async-trait = "0.1.74" # First Party Crates diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 4767e4f8d255..90e5df538055 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -24,7 +24,7 @@ error-stack = "0.3.1" once_cell = "1.18.0" serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" -tokio = "1.28.2" +tokio = "1.35.1" hyper-proxy = "0.9.1" hyper = "0.14.26" diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index 1a6bc96a7fc4..9d2c6042731b 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -13,7 +13,7 @@ fred = { version = "7.0.0", features = ["metrics", "partial-tracing", "subscribe futures = "0.3" serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.40" -tokio = "1.28.2" +tokio = "1.35.1" tokio-stream = {version = "0.1.14", features = ["sync"]} # First party crates @@ -21,4 +21,4 @@ common_utils = { version = "0.1.0", path = "../common_utils", features = ["async router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } [dev-dependencies] -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 88272033fb04..acc6b70a2edd 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -91,7 +91,7 @@ strum = { version = "0.25", features = ["derive"] } tera = "1.19.1" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } unicode-segmentation = "1.10.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } @@ -134,7 +134,7 @@ derive_deref = "1.1.1" rand = "0.8.5" serial_test = "2.0.0" time = { version = "0.3.21", features = ["macros"] } -tokio = "1.28.2" +tokio = "1.35.1" wiremock = "0.5.18" # First party dev-dependencies diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index ae82a0c094dd..8dca7942ab0a 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -21,7 +21,7 @@ serde_json = "1.0.108" serde_path_to_error = "0.1.14" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", default-features = false, features = ["formatting"] } -tokio = { version = "1.28.2" } +tokio = { version = "1.35.1" } tracing = { version = "=0.1.36" } tracing-actix-web = { version = "0.7.8", features = ["opentelemetry_0_19", "uuid_v7"], optional = true } tracing-appender = { version = "0.2.2" } @@ -31,7 +31,7 @@ tracing-subscriber = { version = "0.3.17", default-features = true, features = [ vergen = { version = "8.2.1", optional = true, features = ["cargo", "git", "git2", "rustc"] } [dev-dependencies] -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } [build-dependencies] cargo_metadata = "0.15.4" diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index 40f7ff7b9474..fe090552edb3 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -20,7 +20,7 @@ serde_json = "1.0.108" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } # First party crates diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 0155980d9f7d..c39154d97622 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -44,4 +44,4 @@ ring = "0.16.20" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["rt-multi-thread"] } +tokio = { version = "1.35.1", features = ["rt-multi-thread"] } diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index a95e2e3921bd..d53cdc4ac074 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml @@ -24,7 +24,7 @@ serde_urlencoded = "0.7.1" serial_test = "2.0.0" thirtyfour = "0.31.0" time = { version = "0.3.21", features = ["macros"] } -tokio = "1.28.2" +tokio = "1.35.1" toml = "0.7.4" # First party crates From 7813ceece2081b73f1374e2ee5a9a673f0b72127 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:45:39 +0530 Subject: [PATCH 366/443] feat(core): send `customer_name` to connectors when creating customer (#3380) --- .../router/src/connector/stax/transformers.rs | 2 +- .../src/connector/stripe/transformers.rs | 2 +- .../src/core/payments/flows/authorize_flow.rs | 2 +- .../router/src/core/payments/transformers.rs | 28 ++++++++++++++++++- crates/router/src/types.rs | 18 +++--------- .../router/src/types/api/verify_connector.rs | 1 + crates/router/tests/connectors/aci.rs | 1 + crates/router/tests/connectors/adyen.rs | 1 + crates/router/tests/connectors/bitpay.rs | 1 + crates/router/tests/connectors/cashtocode.rs | 1 + crates/router/tests/connectors/coinbase.rs | 1 + crates/router/tests/connectors/cryptopay.rs | 1 + crates/router/tests/connectors/opennode.rs | 1 + crates/router/tests/connectors/utils.rs | 1 + crates/router/tests/connectors/worldline.rs | 1 + 15 files changed, 44 insertions(+), 18 deletions(-) diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index 01ae751f7487..596ea1145ecc 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -147,7 +147,7 @@ pub struct StaxCustomerRequest { #[serde(skip_serializing_if = "Option::is_none")] email: Option, #[serde(skip_serializing_if = "Option::is_none")] - firstname: Option, + firstname: Option>, } impl TryFrom<&types::ConnectorCustomerRouterData> for StaxCustomerRequest { diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 89e186924142..1dbb310868a6 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -2018,7 +2018,7 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for CustomerRequest { description: item.request.description.to_owned(), email: item.request.email.to_owned(), phone: item.request.phone.to_owned(), - name: item.request.name.to_owned().map(Secret::new), + name: item.request.name.to_owned(), source: item.request.preprocessing_id.to_owned(), }) } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 15c79f4b9d95..c6de222f7d83 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -376,7 +376,7 @@ impl TryFrom<&types::RouterData( connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, - customer: &Option, + customer: &'a Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult> where @@ -89,6 +89,7 @@ where connector_name: connector_id.to_string(), payment_data: payment_data.clone(), state, + customer_data: customer, }; let customer_id = customer.to_owned().map(|customer| customer.customer_id); @@ -968,6 +969,7 @@ where connector_name: String, payment_data: PaymentData, state: &'a AppState, + customer_data: &'a Option, } impl TryFrom> for types::PaymentsAuthorizeData { type Error = error_stack::Report; @@ -1048,6 +1050,17 @@ impl TryFrom> for types::PaymentsAuthoriz .as_ref() .map(|surcharge_details| surcharge_details.final_amount) .unwrap_or(payment_data.amount.into()); + + let customer_name = additional_data + .customer_data + .as_ref() + .and_then(|customer_data| { + customer_data + .name + .as_ref() + .map(|customer| customer.clone().into_inner()) + }); + Ok(Self { payment_method_data: payment_method_data.get_required_value("payment_method_data")?, setup_future_usage: payment_data.payment_intent.setup_future_usage, @@ -1062,6 +1075,7 @@ impl TryFrom> for types::PaymentsAuthoriz currency: payment_data.currency, browser_info, email: payment_data.email, + customer_name, payment_experience: payment_data.payment_attempt.payment_experience, order_details, order_category, @@ -1354,6 +1368,17 @@ impl TryFrom> for types::SetupMandateRequ .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })?; + + let customer_name = additional_data + .customer_data + .as_ref() + .and_then(|customer_data| { + customer_data + .name + .as_ref() + .map(|customer| customer.clone().into_inner()) + }); + Ok(Self { currency: payment_data.currency, confirm: true, @@ -1368,6 +1393,7 @@ impl TryFrom> for types::SetupMandateRequ setup_mandate_details: payment_data.setup_mandate, router_return_url, email: payment_data.email, + customer_name, return_url: payment_data.payment_intent.return_url, browser_info, payment_method_type: attempt.payment_method_type, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index e236113e6768..0809ca178203 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -392,6 +392,7 @@ pub struct PaymentsAuthorizeData { /// ``` pub amount: i64, pub email: Option, + pub customer_name: Option>, pub currency: storage_enums::Currency, pub confirm: bool, pub statement_descriptor_suffix: Option, @@ -461,7 +462,7 @@ pub struct ConnectorCustomerData { pub description: Option, pub email: Option, pub phone: Option>, - pub name: Option, + pub name: Option>, pub preprocessing_id: Option, pub payment_method_data: payments::PaymentMethodData, } @@ -586,6 +587,7 @@ pub struct SetupMandateRequestData { pub router_return_url: Option, pub browser_info: Option, pub email: Option, + pub customer_name: Option>, pub return_url: Option, pub payment_method_type: Option, pub request_incremental_authorization: bool, @@ -1342,19 +1344,6 @@ impl From<&&mut PaymentsAuthorizeRouterData> for AuthorizeSessionTokenData { } } -impl From<&&mut PaymentsAuthorizeRouterData> for ConnectorCustomerData { - fn from(data: &&mut PaymentsAuthorizeRouterData) -> Self { - Self { - email: data.request.email.to_owned(), - preprocessing_id: data.preprocessing_id.to_owned(), - payment_method_data: data.request.payment_method_data.to_owned(), - description: None, - phone: None, - name: None, - } - } -} - impl From<&RouterData> for PaymentMethodTokenizationData { @@ -1411,6 +1400,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { setup_mandate_details: data.request.setup_mandate_details.clone(), router_return_url: data.request.router_return_url.clone(), email: data.request.email.clone(), + customer_name: data.request.customer_name.clone(), amount: 0, statement_descriptor: None, capture_method: None, diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index c5fcce8b185e..fbd942305845 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -24,6 +24,7 @@ impl VerifyConnectorData { types::PaymentsAuthorizeData { payment_method_data: api::PaymentMethodData::Card(self.card_details.clone()), email: None, + customer_name: None, amount: 1000, confirm: true, currency: storage_enums::Currency::USD, diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 35c9cbd952d3..c820b7acd6e4 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -59,6 +59,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { order_details: None, order_category: None, email: None, + customer_name: None, session_token: None, enrolled_for_3ds: false, related_transaction_id: None, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 490750805062..430ae0bac147 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -147,6 +147,7 @@ impl AdyenTest { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 8bac7c13c85f..892d5b1f208f 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -81,6 +81,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index 68c4eb94bf32..9d0824457199 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -57,6 +57,7 @@ impl CashtocodeTest { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type, session_token: None, diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index 73ee93178c01..9a476df7fe63 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -83,6 +83,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index 5df8d80461fa..5e1b3f5ab47b 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -81,6 +81,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index b140a7c05170..69edec2af2cf 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -82,6 +82,7 @@ fn payment_method_details() -> Option { order_details: None, order_category: None, email: None, + customer_name: None, payment_experience: None, payment_method_type: None, session_token: None, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index db82cd7e0324..ed3cdbe31b52 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -900,6 +900,7 @@ impl Default for PaymentAuthorizeType { order_details: None, order_category: None, email: None, + customer_name: None, session_token: None, enrolled_for_3ds: false, related_transaction_id: None, diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 4f7a94780a59..8b8657890039 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -92,6 +92,7 @@ impl WorldlineTest { order_details: None, order_category: None, email: None, + customer_name: None, session_token: None, enrolled_for_3ds: false, related_transaction_id: None, From 25790a161aebe86d58edb6feafce821a77b69dd4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:20:30 +0000 Subject: [PATCH 367/443] chore(version): 2024.01.22.1 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d650f3fb81..a964c6b1748b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.22.1 + +### Features + +- **core:** Send `customer_name` to connectors when creating customer ([#3380](https://github.com/juspay/hyperswitch/pull/3380)) ([`7813cee`](https://github.com/juspay/hyperswitch/commit/7813ceece2081b73f1374e2ee5a9a673f0b72127)) + +### Miscellaneous Tasks + +- Chore(deps): bump the cargo group across 1 directories with 3 updates ([#3409](https://github.com/juspay/hyperswitch/pull/3409)) ([`6c46e9c`](https://github.com/juspay/hyperswitch/commit/6c46e9c19b304bb11f304e60c46e8abf67accf6d)) + +**Full Changelog:** [`2024.01.22.0...2024.01.22.1`](https://github.com/juspay/hyperswitch/compare/2024.01.22.0...2024.01.22.1) + +- - - + ## 2024.01.22.0 ### Features From 4a8104e5f8dd2cfd03de4055baf1256cb7533895 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:04:42 +0530 Subject: [PATCH 368/443] feat(compatibility): add multiuse mandates support in stripe compatibility (#3425) --- .../stripe/payment_intents/types.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 38007a3110d6..51f938d445c4 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -738,9 +738,25 @@ impl ForeignTryFrom<(Option, Option)> for Option Some(payments::MandateType::MultiUse(None)), + StripeMandateType::MultiUse => Some(payments::MandateType::MultiUse(Some( + payments::MandateAmountData { + amount: mandate.amount.unwrap_or_default(), + currency, + start_date: mandate.start_date, + end_date: mandate.end_date, + metadata: None, + }, + ))), }, - None => Some(api_models::payments::MandateType::MultiUse(None)), + None => Some(api_models::payments::MandateType::MultiUse(Some( + payments::MandateAmountData { + amount: mandate.amount.unwrap_or_default(), + currency, + start_date: mandate.start_date, + end_date: mandate.end_date, + metadata: None, + }, + ))), }, customer_acceptance: Some(payments::CustomerAcceptance { acceptance_type: payments::AcceptanceType::Online, From d2c3a830ad6ef5c317928949f7f1b20c2f4abb87 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:31:23 +0000 Subject: [PATCH 369/443] chore(version): 2024.01.23.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a964c6b1748b..800218b8a437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.23.0 + +### Features + +- **compatibility:** Add multiuse mandates support in stripe compatibility ([#3425](https://github.com/juspay/hyperswitch/pull/3425)) ([`4a8104e`](https://github.com/juspay/hyperswitch/commit/4a8104e5f8dd2cfd03de4055baf1256cb7533895)) + +**Full Changelog:** [`2024.01.22.1...2024.01.23.0`](https://github.com/juspay/hyperswitch/compare/2024.01.22.1...2024.01.23.0) + +- - - + ## 2024.01.22.1 ### Features From 8551c72fd8cc95ea5de08eb5491bfd43a0c142b1 Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:37:04 +0530 Subject: [PATCH 370/443] ci(postman): Fix session call in Stripe collection (#3430) --- .../Payment Connector - Create/request.json | 15 --------------- .../Payment Connector - Create/request.json | 2 +- .../Payment Connector - Create/request.json | 2 +- .../Payment Connector - Create/request.json | 2 +- .../Payment Connector - Create/request.json | 2 +- .../Payment Connector - Update/request.json | 2 +- .../Payout Connector - Create/request.json | 2 +- 7 files changed, 6 insertions(+), 21 deletions(-) diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 375c1f0df52f..b5002aeca8e7 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -321,21 +321,6 @@ "merchant_info": { "merchant_name": "Narayan Bhat" } - }, - "apple_pay": { - "session_token_data": { - "initiative": "web", - "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", - "display_name": "applepay", - "certificate_keys": "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==", - "initiative_context": "hyperswitch-sdk-test.netlify.app", - "merchant_identifier": "merchant.com.adyen.san" - }, - "payment_request_data": { - "label": "applepay pvt.ltd", - "supported_networks": ["visa", "masterCard", "amex", "discover"], - "merchant_capabilities": ["supports3DS"] - } } } } diff --git a/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json index 7eab20001651..7e3f37901512 100644 --- a/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/hyperswitch/Hackathon/QuickStart/Payment Connector - Create/request.json @@ -286,7 +286,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json index 7eab20001651..7e3f37901512 100644 --- a/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/hyperswitch/QuickStart/Payment Connector - Create/request.json @@ -286,7 +286,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 291a5bf84b51..69e35cff273c 100644 --- a/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -308,7 +308,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json index 9d742acdf3c6..3a281d4004a9 100644 --- a/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/stripe/QuickStart/Payment Connector - Create/request.json @@ -291,7 +291,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json b/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json index ef812ef5d172..766fd2508190 100644 --- a/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json +++ b/postman/collection-dir/stripe/QuickStart/Payment Connector - Update/request.json @@ -288,7 +288,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { diff --git a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json index 817114b426a7..6b4757c18c39 100644 --- a/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json +++ b/postman/collection-dir/wise/Flow Testcases/QuickStart/Payout Connector - Create/request.json @@ -305,7 +305,7 @@ "certificate": "{{certificate}}", "display_name": "applepay", "certificate_keys": "{{certificate_keys}}", - "initiative_context": "hyperswitch-sdk-test.netlify.app", + "initiative_context": "sdk-test-app.netlify.app", "merchant_identifier": "merchant.com.stripe.sang" }, "payment_request_data": { From 7885b2a213f474da3e018ddeb56bc6e407c48471 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 00:18:36 +0000 Subject: [PATCH 371/443] chore(postman): update Postman collection files --- postman/collection-json/adyen_uk.postman_collection.json | 2 +- postman/collection-json/hyperswitch.postman_collection.json | 4 ++-- postman/collection-json/stripe.postman_collection.json | 6 +++--- postman/collection-json/wise.postman_collection.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 26963aa8abbe..91a03afa47c8 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -472,7 +472,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdKakNDQlE2Z0F3SUJBZ0lRRENzRmFrVkNLU01uc2JacTc1YTI0ekFOQmdrcWhraUc5dzBCQVFzRkFEQjEKTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRApaWEowYVdacFkyRjBhVzl1SUVGMWRHaHZjbWwwZVRFTE1Ba0dBMVVFQ3d3Q1J6TXhFekFSQmdOVkJBb01Da0Z3CmNHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeU1USXdPREE1TVRJeE1Wb1hEVEkxTURFd05qQTUKTVRJeE1Gb3dnYWd4SmpBa0Jnb0praWFKay9Jc1pBRUJEQlp0WlhKamFHRnVkQzVqYjIwdVlXUjVaVzR1YzJGdQpNVHN3T1FZRFZRUUREREpCY0hCc1pTQlFZWGtnVFdWeVkyaGhiblFnU1dSbGJuUnBkSGs2YldWeVkyaGhiblF1ClkyOXRMbUZrZVdWdUxuTmhiakVUTUJFR0ExVUVDd3dLV1UwNVZUY3pXakpLVFRFc01Db0dBMVVFQ2d3alNsVlQKVUVGWklGUkZRMGhPVDB4UFIwbEZVeUJRVWtsV1FWUkZJRXhKVFVsVVJVUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRDhIUy81ZmJZNVJLaElYU3pySEpoeTVrNmY0YUdMaEltYklLaXFYRUlUCnVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYzV5eGE0cHg5eHlmQlVIejhzeU9pMjdYNVZaVG8KTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVZlhQWHBjdjFqVVRyRCtlc1RJTFZUb1FUTmhDcwplQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWNaWC9vWTB1R040VWd4c0JYWHdZM0dKbTFSQ3B1CjM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSEhNMGpEQ2lncVU1RktwL1pBbHdzYmg1WVZOU00KWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nV1Y0Z0hUNmhBZ01CQUFHamdnSjhNSUlDZURBTQpCZ05WSFJNQkFmOEVBakFBTUI4R0ExVWRJd1FZTUJhQUZBbit3QldRK2E5a0NwSVN1U1lvWXd5WDdLZXlNSEFHCkNDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnYKYlM5M2QyUnlaek11WkdWeU1ERUdDQ3NHQVFVRkJ6QUJoaVZvZEhSd09pOHZiMk56Y0M1aGNIQnNaUzVqYjIwdgpiMk56Y0RBekxYZDNaSEpuTXpBNU1JSUJMUVlEVlIwZ0JJSUJKRENDQVNBd2dnRWNCZ2txaGtpRzkyTmtCUUV3CmdnRU5NSUhSQmdnckJnRUZCUWNDQWpDQnhBeUJ3VkpsYkdsaGJtTmxJRzl1SUhSb2FYTWdRMlZ5ZEdsbWFXTmgKZEdVZ1lua2dZVzU1SUhCaGNuUjVJRzkwYUdWeUlIUm9ZVzRnUVhCd2JHVWdhWE1nY0hKdmFHbGlhWFJsWkM0ZwpVbVZtWlhJZ2RHOGdkR2hsSUdGd2NHeHBZMkZpYkdVZ2MzUmhibVJoY21RZ2RHVnliWE1nWVc1a0lHTnZibVJwCmRHbHZibk1nYjJZZ2RYTmxMQ0JqWlhKMGFXWnBZMkYwWlNCd2IyeHBZM2tnWVc1a0lHTmxjblJwWm1sallYUnAKYjI0Z2NISmhZM1JwWTJVZ2MzUmhkR1Z0Wlc1MGN5NHdOd1lJS3dZQkJRVUhBZ0VXSzJoMGRIQnpPaTh2ZDNkMwpMbUZ3Y0d4bExtTnZiUzlqWlhKMGFXWnBZMkYwWldGMWRHaHZjbWwwZVM4d0V3WURWUjBsQkF3d0NnWUlLd1lCCkJRVUhBd0l3SFFZRFZSME9CQllFRk5RSysxcUNHbDRTQ1p6SzFSUmpnb05nM0hmdk1BNEdBMVVkRHdFQi93UUUKQXdJSGdEQlBCZ2txaGtpRzkyTmtCaUFFUWd4QVFVUkNRemxDTmtGRE5USkVRems0TnpCRk5qYzJNVFpFUkRJdwpPVUkwTWtReE1UVXlSVVpFTURVeFFVRXhRekV6T0ROR00wUkROa1V5TkVNelFqRkVSVEFQQmdrcWhraUc5Mk5rCkJpNEVBZ1VBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBSFR6NTU2RUs5VVp6R0RVd2cvcmFibmYrUXFSYkgKcllVS0ZNcWQwUDhFTHZGMmYrTzN0ZXlDWHNBckF4TmVMY2hRSGVTNUFJOHd2azdMQ0xLUmJCdWJQQy9NVmtBKwpCZ3h5STg2ejJOVUNDWml4QVM1d2JFQWJYOStVMFp2RHp5Y01BbUNrdVVHZjNwWXR5TDNDaEplSGRvOEwwdmdvCnJQWElUSzc4ZjQzenNzYjBTNE5xbTE0eS9LNCs1ZkUzcFUxdEJqME5tUmdKUVJLRnB6MENpV2RPd1BRTk5BYUMKYUNNU2NiYXdwUTBjWEhaZDJWVjNtem4xdUlITEVtaU5GTWVxeEprbjZhUXFFQnNndDUzaUFxcmZMNjEzWStScAppd0tENXVmeU0wYzBweTYyZmkvWEwwS2c4ajEwWU1VdWJpd2dHajAzZThQWTB6bWUvcGZCZ3p6VQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\",\"display_name\":\"applepay\",\"certificate_keys\":\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRDhIUy81ZmJZNVJLaEkKWFN6ckhKaHk1azZmNGFHTGhJbWJJS2lxWEVJVHVSQ2RHcGcyMExZM1VhTlBlYXZXTVRIUTBpK3d1RzlZWFVhYwo1eXhhNHB4OXh5ZkJVSHo4c3lPaTI3WDVWWlRvTlFhd2F6dGM5aGpZc1B2K0s2UW9oaWRTQWZ3cDhMdThkQ0lVCmZYUFhwY3YxalVUckQrZXNUSUxWVG9RVE5oQ3NlQlJtUS9nK05WdTB5c3BqeUYxU2l6VG9BK1BML3NrMlJEYWMKWlgvb1kwdUdONFVneHNCWFh3WTNHSm0xUkNwdTM0Y2d0UC9kaHNBM1Ixb1VOb0gyQkZBSm9xK3pyUnl3U1RCSApITTBqRENpZ3FVNUZLcC9aQWx3c2JoNVlWTlNNWksrQ0pTK1BPTzlVNGVkeHJmTGlBVkhnQTgzRG43Z2U4K29nCldWNGdIVDZoQWdNQkFBRUNnZ0VBZFNaRzVhTFJxdmpKVFo3bVFYWHZMT3p4dWY5SlpxQTJwUHZmQkJLTXJjZC8KL2RDZXpGSGRhZ1VvWXNUQjRXekluaVVjL2Z3bDJTUzJyREFMZjB2dnRjNTJIYkQ5OHhwMnc3VmVjTGFnMCtuWAo2dUJaSEZCS3FWNU1LZ1l6YUpVMTdqaDM2VEV3dTFnbmdlZnRqVlpBV1NERTFvbDBlSzZ3Mk5kOExjVWdxRkxUCjVHYUlBV01nd0NKL3pzQmwydUV1Y0Q4S21WL1Z2MkVCQVJLWGZtci92UU1NelZrNkhhalprVGZqbWY2cWFVQVMKQWlFblROcHBic2ZrdTk2VGdIa2owWm10VWc0SFkzSU9qWFlpaGJsSjJzQ1JjS3p6cXkxa3B3WlpHcHo1NXEzbgphSXEwenJ3RjlpTUZubEhCa04yK3FjSnhzcDNTalhRdFRLTTY4WHRrVlFLQmdRRC8wemtCVlExR2Q1U0Mzb2czCnM3QWRCZ243dnVMdUZHZFFZY3c0OUppRGw1a1BZRXlKdGQvTVpNOEpFdk1nbVVTeUZmczNZcGtmQ2VGbUp0S3QKMnNNNEdCRWxqTVBQNjI3Q0QrV3c4L3JpWmlOZEg3OUhPRjRneTRGbjBycDNqanlLSWF1OHJISDQwRUUvSkVyOQpxWFQ1SGdWMmJQOGhMcW5sSjFmSDhpY2Zkd0tCZ1FEOFNWQ3ZDV2txQkh2SzE5ZDVTNlArdm5hcXhnTWo0U0srCnJ6L1I1c3pTaW5lS045VEhyeVkxYUZJbVFJZjJYOUdaQXBocUhrckxtQ3BIcURHOWQ3WDVQdUxxQzhmc09kVTYKRzhWaFRXeXdaSVNjdGRSYkk5S2xKUFk2V2ZDQjk0ODNVaDJGbW1xV2JuNWcwZUJxZWZzamVVdEtHekNRaGJDYworR1dBREVRSXB3S0JnUURmaWYvN3pBZm5sVUh1QU9saVV0OEczV29IMGtxVTRydE1IOGpGMCtVWXgzVDFYSjVFCmp1blp2aFN5eHg0dlUvNFU1dVEzQnk3cFVrYmtiZlFWK2x3dlBjaHQyVXlZK0E0MkFKSWlSMjdvT1h1Wk9jNTQKT3liMDNSNWNUR1NuWjJBN0N5VDNubStRak5rV2hXNEpyUE1MWTFJK293dGtRVlF2YW10bnlZNnFEUUtCZ0ZYWgpLT0IzSmxjSzhZa0R5Nm5WeUhkZUhvbGNHaU55YjkxTlN6MUUrWHZIYklnWEdZdmRtUFhoaXRyRGFNQzR1RjBGCjJoRjZQMTlxWnpDOUZqZnY3WGRrSTlrYXF5eENQY0dwUTVBcHhZdDhtUGV1bEJWemFqR1NFMHVsNFVhSWxDNXgKL2VQQnVQVjVvZjJXVFhST0Q5eHhZT0pWd0QvZGprekw1ZFlkMW1UUEFvR0JBTWVwY3diVEphZ3BoZk5zOHY0WAprclNoWXplbVpxY2EwQzRFc2QwNGYwTUxHSlVNS3Zpck4zN0g1OUFjT2IvNWtZcTU5WFRwRmJPWjdmYlpHdDZnCkxnM2hWSHRacElOVGJ5Ni9GOTBUZ09Za3RxUnhNVmc3UFBxbjFqdEFiVU15eVpVZFdHcFNNMmI0bXQ5dGhlUDEKblhMR09NWUtnS2JYbjZXWWN5K2U5eW9ICi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0KCg==\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.adyen.san\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"adyen\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\",\"api_secret\":\"{{connector_api_secret}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"gift_card\",\"payment_method_types\":[{\"payment_method_type\":\"givex\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", diff --git a/postman/collection-json/hyperswitch.postman_collection.json b/postman/collection-json/hyperswitch.postman_collection.json index ab710ca4316a..0acf2ee2b3fc 100644 --- a/postman/collection-json/hyperswitch.postman_collection.json +++ b/postman/collection-json/hyperswitch.postman_collection.json @@ -1915,7 +1915,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -4798,7 +4798,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index e158ccd1a5eb..0638ff734c4a 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -2079,7 +2079,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -2349,7 +2349,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", @@ -5664,7 +5664,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"card_networks\":[\"AmericanExpress\",\"Discover\",\"Interac\",\"JCB\",\"Mastercard\",\"Visa\",\"DinersClub\",\"UnionPay\",\"RuPay\"]}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", diff --git a/postman/collection-json/wise.postman_collection.json b/postman/collection-json/wise.postman_collection.json index dc4d9395d3ac..410f066ff6fb 100644 --- a/postman/collection-json/wise.postman_collection.json +++ b/postman/collection-json/wise.postman_collection.json @@ -424,7 +424,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"wise\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"payout_processor\",\"connector_name\":\"wise\",\"connector_account_details\":{\"auth_type\":\"BodyKey\",\"api_key\":\"{{connector_api_key}}\",\"key1\":\"{{connector_key1}}\"},\"test_mode\":false,\"disabled\":false,\"business_country\":\"US\",\"business_label\":\"default\",\"payment_methods_enabled\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\",\"Mastercard\"],\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"payment_method_type\":\"klarna\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"affirm\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"afterpay_clearpay\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"pay_bright\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"walley\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"paypal\",\"payment_experience\":\"redirect_to_url\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mobile_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"ali_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"we_chat_pay\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"mb_way\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"giropay\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"blik\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"trustly\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_czech_republic\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_finland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_poland\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"online_banking_slovakia\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bancontact_card\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\"}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"sdk-test-app.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", From cc7e33a5751d97b44c7aba561c974f529ce8824a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 04:03:19 +0000 Subject: [PATCH 372/443] chore(version): 2024.01.24.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 800218b8a437..af517a6a1153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.24.0 + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`7885b2a`](https://github.com/juspay/hyperswitch/commit/7885b2a213f474da3e018ddeb56bc6e407c48471)) + +**Full Changelog:** [`2024.01.23.0...2024.01.24.0`](https://github.com/juspay/hyperswitch/compare/2024.01.23.0...2024.01.24.0) + +- - - + ## 2024.01.23.0 ### Features From 629d546aa7c774e86d609abec3b3ab5cf0d100a7 Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Wed, 24 Jan 2024 14:06:52 +0530 Subject: [PATCH 373/443] feat(hashicorp): implement hashicorp secrets manager solution (#3297) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .typos.toml | 1 + Cargo.lock | 192 ++++++++++++++-- Makefile | 9 +- crates/drainer/Cargo.toml | 1 + crates/drainer/src/connection.rs | 15 +- crates/drainer/src/services.rs | 10 + crates/drainer/src/settings.rs | 4 + crates/external_services/Cargo.toml | 3 + .../external_services/src/hashicorp_vault.rs | 215 ++++++++++++++++++ .../src/hashicorp_vault/decrypt.rs | 50 ++++ crates/external_services/src/kms.rs | 38 ++++ crates/external_services/src/lib.rs | 3 + crates/router/Cargo.toml | 1 + crates/router/src/configs.rs | 2 + crates/router/src/configs/hc_vault.rs | 134 +++++++++++ crates/router/src/configs/settings.rs | 4 + crates/router/src/core/api_keys.rs | 36 ++- crates/router/src/core/currency.rs | 4 + crates/router/src/core/payments.rs | 2 +- .../src/core/payments/flows/session_flow.rs | 103 +++++++-- crates/router/src/core/payments/helpers.rs | 60 ++++- crates/router/src/core/pm_auth.rs | 27 ++- crates/router/src/routes/api_keys.rs | 12 + crates/router/src/routes/app.rs | 48 +++- crates/router/src/services.rs | 76 ++++--- crates/router/src/services/authentication.rs | 27 ++- crates/router/src/utils/currency.rs | 95 +++++++- crates/router_env/Cargo.toml | 6 +- 28 files changed, 1094 insertions(+), 84 deletions(-) create mode 100644 crates/external_services/src/hashicorp_vault.rs create mode 100644 crates/external_services/src/hashicorp_vault/decrypt.rs create mode 100644 crates/router/src/configs/hc_vault.rs diff --git a/.typos.toml b/.typos.toml index 4ce21526604b..40acb1305892 100644 --- a/.typos.toml +++ b/.typos.toml @@ -36,6 +36,7 @@ ba = "ba" # ignore minor commit conversions ede = "ede" # ignore minor commit conversions daa = "daa" # Commit id afe = "afe" # Commit id +Hashi = "Hashi" # HashiCorp [files] extend-exclude = [ diff --git a/Cargo.lock b/Cargo.lock index 2cd3cd65c318..5623fd9f729f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" dependencies = [ - "darling", + "darling 0.20.3", "parse-size", "proc-macro2", "quote", @@ -189,10 +189,10 @@ dependencies = [ "rustls 0.21.7", "rustls-webpki", "tokio 1.35.1", - "tokio-rustls", + "tokio-rustls 0.23.4", "tokio-util", "tracing", - "webpki-roots", + "webpki-roots 0.22.6", ] [[package]] @@ -954,7 +954,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "lazy_static", "pin-project-lite", "rustls 0.20.9", @@ -1990,14 +1990,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", ] [[package]] @@ -2014,13 +2038,24 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core", + "darling_core 0.20.3", "quote", "syn 2.0.48", ] @@ -2104,6 +2139,37 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "derive_deref" version = "1.1.1" @@ -2421,6 +2487,7 @@ dependencies = [ "common_utils", "dyn-clone", "error-stack", + "hex", "hyper", "hyper-proxy", "masking", @@ -2429,6 +2496,7 @@ dependencies = [ "serde", "thiserror", "tokio 1.35.1", + "vaultrs", ] [[package]] @@ -2453,7 +2521,7 @@ dependencies = [ "futures-util", "http", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "mime", "serde", "serde_json", @@ -3098,7 +3166,21 @@ dependencies = [ "rustls 0.20.9", "rustls-native-certs", "tokio 1.35.1", - "tokio-rustls", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.7", + "tokio 1.35.1", + "tokio-rustls 0.24.1", ] [[package]] @@ -4945,6 +5027,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -4955,18 +5038,22 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.7", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "system-configuration", "tokio 1.35.1", "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 0.25.3", "winreg", ] @@ -5302,6 +5389,40 @@ dependencies = [ "nom", ] +[[package]] +name = "rustify" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c02e25271068de581e03ac3bb44db60165ff1a10d92b9530192ccb898bc706" +dependencies = [ + "anyhow", + "async-trait", + "bytes 1.5.0", + "http", + "reqwest", + "rustify_derive", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "rustify_derive" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58135536c18c04f4634bedad182a3f41baf33ef811cc38a3ec7b7061c57134c8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "serde_urlencoded", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "rustix" version = "0.38.28" @@ -5670,7 +5791,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ - "darling", + "darling 0.20.3", "proc-macro2", "quote", "syn 2.0.48", @@ -6561,6 +6682,16 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.7", + "tokio 1.35.1", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -6794,11 +6925,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -6833,20 +6963,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -7155,6 +7285,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vaultrs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28084ac780b443e7f3514df984a2933bd3ab39e71914d951cdf8e4d298a7c9bc" +dependencies = [ + "async-trait", + "bytes 1.5.0", + "derive_builder", + "http", + "reqwest", + "rustify", + "rustify_derive", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -7346,6 +7496,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "weezl" version = "0.1.7" diff --git a/Makefile b/Makefile index 780d5a993c92..a39fc4c22673 100644 --- a/Makefile +++ b/Makefile @@ -34,11 +34,18 @@ ROOT_DIR := $(realpath $(ROOT_DIR_WITH_SLASH)) release +# Check a local package and all of its dependencies for errors +# +# Usage : +# make check +check: + cargo check + + # Compile application for running on local machine # # Usage : # make build - build : cargo build diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index f26c31f0e72c..67169a151044 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] release = ["kms", "vergen"] kms = ["external_services/kms"] +hashicorp-vault = ["external_services/hashicorp-vault"] vergen = ["router_env/vergen"] [dependencies] diff --git a/crates/drainer/src/connection.rs b/crates/drainer/src/connection.rs index 7b273244cbce..6af0a9782232 100644 --- a/crates/drainer/src/connection.rs +++ b/crates/drainer/src/connection.rs @@ -1,5 +1,7 @@ use bb8::PooledConnection; use diesel::PgConnection; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch, Kv2}; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; #[cfg(not(feature = "kms"))] @@ -27,16 +29,23 @@ pub async fn diesel_make_pg_pool( database: &Database, _test_transaction: bool, #[cfg(feature = "kms")] kms_client: &'static kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] hashicorp_client: &'static hashicorp_vault::HashiCorpVault, ) -> PgPool { + let password = database.password.clone(); + #[cfg(feature = "hashicorp-vault")] + let password = password + .fetch_inner::(hashicorp_client) + .await + .expect("Failed while fetching db password"); + #[cfg(feature = "kms")] - let password = database - .password + let password = password .decrypt_inner(kms_client) .await .expect("Failed to decrypt password"); #[cfg(not(feature = "kms"))] - let password = &database.password.peek(); + let password = &password.peek(); let database_url = format!( "postgres://{}:{}@{}:{}/{}", diff --git a/crates/drainer/src/services.rs b/crates/drainer/src/services.rs index 481fcc07221c..4393ebb9dc97 100644 --- a/crates/drainer/src/services.rs +++ b/crates/drainer/src/services.rs @@ -17,6 +17,11 @@ pub struct StoreConfig { } impl Store { + /// # Panics + /// + /// Panics if there is a failure while obtaining the HashiCorp client using the provided configuration. + /// This panic indicates a critical failure in setting up external services, and the application cannot proceed without a valid HashiCorp client. + /// pub async fn new(config: &crate::settings::Settings, test_transaction: bool) -> Self { Self { master_pool: diesel_make_pg_pool( @@ -24,6 +29,11 @@ impl Store { test_transaction, #[cfg(feature = "kms")] external_services::kms::get_kms_client(&config.kms).await, + #[cfg(feature = "hashicorp-vault")] + #[allow(clippy::expect_used)] + external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .expect("Failed while getting hashicorp client"), ) .await, redis_conn: Arc::new(crate::connection::redis_connection(config).await), diff --git a/crates/drainer/src/settings.rs b/crates/drainer/src/settings.rs index 8101abf5028e..5b80ee375f54 100644 --- a/crates/drainer/src/settings.rs +++ b/crates/drainer/src/settings.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; #[cfg(feature = "kms")] use external_services::kms; use redis_interface as redis; @@ -34,6 +36,8 @@ pub struct Settings { pub drainer: DrainerSettings, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 90e5df538055..6552b57b0e54 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] kms = ["dep:aws-config", "dep:aws-sdk-kms"] email = ["dep:aws-config"] +hashicorp-vault = [ "dep:vaultrs" ] [dependencies] async-trait = "0.1.68" @@ -27,6 +28,8 @@ thiserror = "1.0.40" tokio = "1.35.1" hyper-proxy = "0.9.1" hyper = "0.14.26" +vaultrs = { version = "0.7.0", optional = true } +hex = "0.4.3" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } diff --git a/crates/external_services/src/hashicorp_vault.rs b/crates/external_services/src/hashicorp_vault.rs new file mode 100644 index 000000000000..e31c8f01392e --- /dev/null +++ b/crates/external_services/src/hashicorp_vault.rs @@ -0,0 +1,215 @@ +//! Interactions with the HashiCorp Vault + +use std::{collections::HashMap, future::Future, pin::Pin}; + +use error_stack::{Report, ResultExt}; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; + +/// Utilities for supporting decryption of data +pub mod decrypt; + +static HC_CLIENT: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); + +#[allow(missing_debug_implementations)] +/// A struct representing a connection to HashiCorp Vault. +pub struct HashiCorpVault { + /// The underlying client used for interacting with HashiCorp Vault. + client: VaultClient, +} + +/// Configuration for connecting to HashiCorp Vault. +#[derive(Clone, Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct HashiCorpVaultConfig { + /// The URL of the HashiCorp Vault server. + pub url: String, + /// The authentication token used to access HashiCorp Vault. + pub token: String, +} + +/// Asynchronously retrieves a HashiCorp Vault client based on the provided configuration. +/// +/// # Parameters +/// +/// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. +pub async fn get_hashicorp_client( + config: &HashiCorpVaultConfig, +) -> error_stack::Result<&'static HashiCorpVault, HashiCorpError> { + HC_CLIENT + .get_or_try_init(|| async { HashiCorpVault::new(config) }) + .await +} + +/// A trait defining an engine for interacting with HashiCorp Vault. +pub trait Engine: Sized { + /// The associated type representing the return type of the engine's operations. + type ReturnType<'b, T> + where + T: 'b, + Self: 'b; + /// Reads data from HashiCorp Vault at the specified location. + /// + /// # Parameters + /// + /// - `client`: A reference to the HashiCorpVault client. + /// - `location`: The location in HashiCorp Vault to read data from. + /// + /// # Returns + /// + /// A future representing the result of the read operation. + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String>; +} + +/// An implementation of the `Engine` trait for the Key-Value version 2 (Kv2) engine. +#[derive(Debug)] +pub enum Kv2 {} + +impl Engine for Kv2 { + type ReturnType<'b, T: 'b> = + Pin> + Send + 'b>>; + fn read(client: &HashiCorpVault, location: String) -> Self::ReturnType<'_, String> { + Box::pin(async move { + let mut split = location.split(':'); + let mount = split.next().ok_or(HashiCorpError::IncompleteData)?; + let path = split.next().ok_or(HashiCorpError::IncompleteData)?; + let key = split.next().unwrap_or("value"); + + let mut output = + vaultrs::kv2::read::>(&client.client, mount, path) + .await + .map_err(Into::>::into) + .change_context(HashiCorpError::FetchFailed)?; + + Ok(output.remove(key).ok_or(HashiCorpError::ParseError)?) + }) + } +} + +impl HashiCorpVault { + /// Creates a new instance of HashiCorpVault based on the provided configuration. + /// + /// # Parameters + /// + /// - `config`: A reference to a `HashiCorpVaultConfig` containing the configuration details. + /// + pub fn new(config: &HashiCorpVaultConfig) -> error_stack::Result { + VaultClient::new( + VaultClientSettingsBuilder::default() + .address(&config.url) + .token(&config.token) + .build() + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .attach_printable("Failed while building vault settings")?, + ) + .map_err(Into::>::into) + .change_context(HashiCorpError::ClientCreationFailed) + .map(|client| Self { client }) + } + + /// Asynchronously fetches data from HashiCorp Vault using the specified engine. + /// + /// # Parameters + /// + /// - `data`: A String representing the location or identifier of the data in HashiCorp Vault. + /// + /// # Type Parameters + /// + /// - `En`: The engine type that implements the `Engine` trait. + /// - `I`: The type that can be constructed from the retrieved encoded data. + /// + pub async fn fetch(&self, data: String) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + I: FromEncoded, + { + let output = En::read(self, data).await?; + I::from_encoded(output).ok_or(error_stack::report!(HashiCorpError::HexDecodingFailed)) + } +} + +/// A trait for types that can be constructed from encoded data in the form of a String. +pub trait FromEncoded: Sized { + /// Constructs an instance of the type from the provided encoded input. + /// + /// # Parameters + /// + /// - `input`: A String containing the encoded data. + /// + /// # Returns + /// + /// An `Option` representing the constructed instance if successful, or `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// # use your_module::{FromEncoded, masking::Secret, Vec}; + /// let secret_instance = Secret::::from_encoded("encoded_secret_string".to_string()); + /// let vec_instance = Vec::::from_encoded("68656c6c6f".to_string()); + /// ``` + fn from_encoded(input: String) -> Option; +} + +impl FromEncoded for masking::Secret { + fn from_encoded(input: String) -> Option { + Some(input.into()) + } +} + +impl FromEncoded for Vec { + fn from_encoded(input: String) -> Option { + hex::decode(input).ok() + } +} + +/// An enumeration representing various errors that can occur in interactions with HashiCorp Vault. +#[derive(Debug, thiserror::Error)] +pub enum HashiCorpError { + /// Failed while creating hashicorp client + #[error("Failed while creating a new client")] + ClientCreationFailed, + + /// Failed while building configurations for hashicorp client + #[error("Failed while building configuration")] + ConfigurationBuildFailed, + + /// Failed while decoding data to hex format + #[error("Failed while decoding hex data")] + HexDecodingFailed, + + /// An error occurred when base64 decoding input data. + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + + /// An error occurred when KMS decrypting input data. + #[error("Failed to KMS decrypt input data")] + DecryptionFailed, + + /// The KMS decrypted output does not include a plaintext output. + #[error("Missing plaintext KMS decryption output")] + MissingPlaintextDecryptionOutput, + + /// An error occurred UTF-8 decoding KMS decrypted output. + #[error("Failed to UTF-8 decode decryption output")] + Utf8DecodingFailed, + + /// Incomplete data provided to fetch data from hasicorp + #[error("Provided information about the value is incomplete")] + IncompleteData, + + /// Failed while fetching data from vault + #[error("Failed while fetching data from the server")] + FetchFailed, + + /// Failed while parsing received data + #[error("Failed while parsing the response")] + ParseError, +} diff --git a/crates/external_services/src/hashicorp_vault/decrypt.rs b/crates/external_services/src/hashicorp_vault/decrypt.rs new file mode 100644 index 000000000000..1bc1b6ffa16e --- /dev/null +++ b/crates/external_services/src/hashicorp_vault/decrypt.rs @@ -0,0 +1,50 @@ +use std::{future::Future, pin::Pin}; + +use masking::ExposeInterface; + +/// A trait for types that can be asynchronously fetched and decrypted from HashiCorp Vault. +#[async_trait::async_trait] +pub trait VaultFetch: Sized { + /// Asynchronously decrypts the inner content of the type. + /// + /// # Returns + /// + /// An `Result` representing the decrypted instance if successful, + /// or an `super::HashiCorpError` with details about the encountered error. + /// + async fn fetch_inner( + self, + client: &super::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a; +} + +#[async_trait::async_trait] +impl VaultFetch for masking::Secret { + async fn fetch_inner( + self, + client: &super::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::Engine< + ReturnType<'a, String> = Pin< + Box< + dyn Future> + + Send + + 'a, + >, + >, + > + 'a, + { + client.fetch::(self.expose()).await + } +} diff --git a/crates/external_services/src/kms.rs b/crates/external_services/src/kms.rs index 04a58e4b23f4..740bca4d821b 100644 --- a/crates/external_services/src/kms.rs +++ b/crates/external_services/src/kms.rs @@ -190,6 +190,44 @@ impl KmsConfig { #[serde(transparent)] pub struct KmsValue(Secret); +impl From for KmsValue { + fn from(value: String) -> Self { + Self(Secret::new(value)) + } +} + +impl From> for KmsValue { + fn from(value: Secret) -> Self { + Self(value) + } +} + +#[cfg(feature = "hashicorp-vault")] +#[async_trait::async_trait] +impl super::hashicorp_vault::decrypt::VaultFetch for KmsValue { + async fn fetch_inner( + self, + client: &super::hashicorp_vault::HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: super::hashicorp_vault::Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result< + String, + super::hashicorp_vault::HashiCorpError, + >, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.0.fetch_inner::(client).await.map(KmsValue) + } +} + impl common_utils::ext_traits::ConfigExt for KmsValue { fn is_empty_after_trim(&self) -> bool { self.0.peek().is_empty_after_trim() diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index ccf1db47a3ae..9bf4916eec33 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -9,6 +9,9 @@ pub mod email; #[cfg(feature = "kms")] pub mod kms; +#[cfg(feature = "hashicorp-vault")] +pub mod hashicorp_vault; + /// Crate specific constants #[cfg(feature = "kms")] pub mod consts { diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index acc6b70a2edd..ef6ea41d524a 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] aws_s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] +hashicorp-vault = ["external_services/hashicorp-vault"] email = ["external_services/email", "dep:aws-config", "olap"] frm = [] stripe = ["dep:serde_qs"] diff --git a/crates/router/src/configs.rs b/crates/router/src/configs.rs index bb8f61646f26..5cb1df0644ee 100644 --- a/crates/router/src/configs.rs +++ b/crates/router/src/configs.rs @@ -1,4 +1,6 @@ mod defaults; +#[cfg(feature = "hashicorp-vault")] +pub mod hc_vault; #[cfg(feature = "kms")] pub mod kms; pub mod settings; diff --git a/crates/router/src/configs/hc_vault.rs b/crates/router/src/configs/hc_vault.rs new file mode 100644 index 000000000000..f20d8e79ed89 --- /dev/null +++ b/crates/router/src/configs/hc_vault.rs @@ -0,0 +1,134 @@ +use external_services::hashicorp_vault::{ + decrypt::VaultFetch, Engine, HashiCorpError, HashiCorpVault, +}; +use masking::ExposeInterface; + +use crate::configs::settings; + +#[async_trait::async_trait] +impl VaultFetch for settings::Jwekey { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + ( + self.vault_encryption_key, + self.rust_locker_encryption_key, + self.vault_private_key, + self.tunnel_private_key, + ) = ( + masking::Secret::new(self.vault_encryption_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.rust_locker_encryption_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.vault_private_key) + .fetch_inner::(client) + .await? + .expose(), + masking::Secret::new(self.tunnel_private_key) + .fetch_inner::(client) + .await? + .expose(), + ); + Ok(self) + } +} + +#[async_trait::async_trait] +impl VaultFetch for settings::Database { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + Ok(Self { + host: self.host, + port: self.port, + dbname: self.dbname, + username: self.username, + password: self.password.fetch_inner::(client).await?, + pool_size: self.pool_size, + connection_timeout: self.connection_timeout, + queue_strategy: self.queue_strategy, + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, + }) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl VaultFetch for settings::PayPalOnboarding { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.client_id = self.client_id.fetch_inner::(client).await?; + self.client_secret = self.client_secret.fetch_inner::(client).await?; + self.partner_id = self.partner_id.fetch_inner::(client).await?; + Ok(self) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl VaultFetch for settings::ConnectorOnboarding { + async fn fetch_inner( + mut self, + client: &HashiCorpVault, + ) -> error_stack::Result + where + for<'a> En: Engine< + ReturnType<'a, String> = std::pin::Pin< + Box< + dyn std::future::Future< + Output = error_stack::Result, + > + Send + + 'a, + >, + >, + > + 'a, + { + self.paypal = self.paypal.fetch_inner::(client).await?; + Ok(self) + } +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index cb4fdd70eb64..3c1d9f7d397e 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -11,6 +11,8 @@ use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; #[cfg(feature = "email")] use external_services::email::EmailSettings; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; #[cfg(feature = "kms")] use external_services::kms; use redis_interface::RedisSettings; @@ -88,6 +90,8 @@ pub struct Settings { pub api_keys: ApiKeys, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, #[cfg(feature = "aws_s3")] pub file_upload_config: FileUploadConfig, pub tokenization: TokenizationConfig, diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index 78d4e801e8f2..f28d845609a1 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -2,8 +2,12 @@ use common_utils::date_time; #[cfg(feature = "email")] use diesel_models::{api_keys::ApiKey, enums as storage_enums}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; +#[cfg(not(feature = "kms"))] +use masking::ExposeInterface; use masking::{PeekInterface, StrongSecret}; use router_env::{instrument, tracing}; @@ -35,19 +39,37 @@ static HASH_KEY: tokio::sync::OnceCell errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> { HASH_KEY .get_or_try_init(|| async { + let hash_key = { + #[cfg(feature = "kms")] + { + api_key_config.kms_encrypted_hash_key.clone() + } + #[cfg(not(feature = "kms"))] + { + masking::Secret::<_, masking::WithType>::new(api_key_config.hash_key.clone()) + } + }; + + #[cfg(feature = "hashicorp-vault")] + let hash_key = hash_key + .fetch_inner::(hc_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + #[cfg(feature = "kms")] - let hash_key = api_key_config - .kms_encrypted_hash_key + let hash_key = hash_key .decrypt_inner(kms_client) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt API key hashing key")?; #[cfg(not(feature = "kms"))] - let hash_key = &api_key_config.hash_key; + let hash_key = hash_key.expose(); <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from( hex::decode(hash_key) @@ -132,6 +154,8 @@ impl PlaintextApiKey { pub async fn create_api_key( state: AppState, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, api_key: api::CreateApiKeyRequest, merchant_id: String, ) -> RouterResponse { @@ -153,6 +177,8 @@ pub async fn create_api_key( api_key_config, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, ) .await?; let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH); @@ -565,6 +591,10 @@ mod tests { &settings.api_keys, #[cfg(feature = "kms")] external_services::kms::get_kms_client(&settings.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&settings.hc_vault) + .await + .unwrap(), ) .await .unwrap(); diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs index 1ea9454f00a0..41699df47a76 100644 --- a/crates/router/src/core/currency.rs +++ b/crates/router/src/core/currency.rs @@ -19,6 +19,8 @@ pub async fn retrieve_forex( state.conf.forex_api.local_fetch_retry_count, #[cfg(feature = "kms")] &state.conf.kms, + #[cfg(feature = "hashicorp-vault")] + &state.conf.hc_vault, ) .await .change_context(ApiErrorResponse::GenericNotFoundError { @@ -44,6 +46,8 @@ pub async fn convert_forex( from_currency, #[cfg(feature = "kms")] &state.conf.kms, + #[cfg(feature = "hashicorp-vault")] + &state.conf.hc_vault, )) .await .change_context(ApiErrorResponse::InternalServerError)?, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 10aa00f5963c..043863a98fa3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -68,7 +68,7 @@ use crate::{ workflows::payment_sync, }; -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::type_complexity)] #[instrument(skip_all, fields(payment_id, merchant_id))] pub async fn payments_operation_core( state: &AppState, diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index de697e02f780..099c266e04f2 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -2,8 +2,14 @@ use api_models::payments as payment_types; use async_trait::async_trait; use common_utils::{ext_traits::ByteSliceExt, request::RequestContent}; use error_stack::{IntoReport, Report, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; +#[cfg(feature = "hashicorp-vault")] +use masking::ExposeInterface; use super::{ConstructFlowSpecificData, Feature}; use crate::{ @@ -177,10 +183,85 @@ async fn create_applepay_session_token( payment_request_data, session_token_data, } => { + let ( + apple_pay_merchant_cert, + apple_pay_merchant_cert_key, + common_merchant_identifier, + ) = async { + #[cfg(feature = "hashicorp-vault")] + let client = external_services::hashicorp_vault::get_hashicorp_client( + &state.conf.hc_vault, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while building hashicorp client")?; + + #[cfg(feature = "hashicorp-vault")] + { + Ok::<_, Report>(( + masking::Secret::new( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + masking::Secret::new( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert_key + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + masking::Secret::new( + state + .conf + .applepay_merchant_configs + .common_merchant_identifier + .clone(), + ) + .fetch_inner::(client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)? + .expose(), + )) + } + + #[cfg(not(feature = "hashicorp-vault"))] + { + Ok::<_, Report>(( + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert + .clone(), + state + .conf + .applepay_decrypt_keys + .apple_pay_merchant_cert_key + .clone(), + state + .conf + .applepay_merchant_configs + .common_merchant_identifier + .clone(), + )) + } + } + .await?; + #[cfg(feature = "kms")] let decrypted_apple_pay_merchant_cert = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert) + .decrypt(apple_pay_merchant_cert) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Apple pay merchant certificate decryption failed")?; @@ -189,7 +270,7 @@ async fn create_applepay_session_token( let decrypted_apple_pay_merchant_cert_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key) + .decrypt(apple_pay_merchant_cert_key) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( @@ -199,21 +280,13 @@ async fn create_applepay_session_token( #[cfg(feature = "kms")] let decrypted_merchant_identifier = kms::get_kms_client(&state.conf.kms) .await - .decrypt( - &state - .conf - .applepay_merchant_configs - .common_merchant_identifier, - ) + .decrypt(common_merchant_identifier) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Apple pay merchant identifier decryption failed")?; #[cfg(not(feature = "kms"))] - let decrypted_merchant_identifier = &state - .conf - .applepay_merchant_configs - .common_merchant_identifier; + let decrypted_merchant_identifier = common_merchant_identifier; let apple_pay_session_request = get_session_request_for_simplified_apple_pay( decrypted_merchant_identifier.to_string(), @@ -221,12 +294,10 @@ async fn create_applepay_session_token( ); #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_merchant_cert = - &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert; + let decrypted_apple_pay_merchant_cert = apple_pay_merchant_cert; #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_merchant_cert_key = - &state.conf.applepay_decrypt_keys.apple_pay_merchant_cert_key; + let decrypted_apple_pay_merchant_cert_key = apple_pay_merchant_cert_key; ( payment_request_data, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 9d3da6c78e4a..213adc79fb01 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -13,6 +13,10 @@ use data_models::{ use diesel_models::enums; // TODO : Evaluate all the helper functions () use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms; use josekit::jwe; @@ -1250,6 +1254,7 @@ pub async fn get_connector_default( } #[instrument(skip_all)] +#[allow(clippy::type_complexity)] pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( operation: BoxedOperation<'a, F, R, Ctx>, db: &dyn StorageInterface, @@ -3501,15 +3506,38 @@ impl ApplePayData { &self, state: &AppState, ) -> CustomResult { + let apple_pay_ppc = async { + #[cfg(feature = "hashicorp-vault")] + let client = + external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = + masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc.clone()) + .fetch_inner::(client) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed)? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.applepay_decrypt_keys.apple_pay_ppc.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let cert_data = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc) + .decrypt(&apple_pay_ppc) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; #[cfg(not(feature = "kms"))] - let cert_data = &state.conf.applepay_decrypt_keys.apple_pay_ppc; + let cert_data = &apple_pay_ppc; let base64_decode_cert_data = BASE64_ENGINE .decode(cert_data) @@ -3561,15 +3589,39 @@ impl ApplePayData { .change_context(errors::ApplePayDecryptionError::KeyDeserializationFailed) .attach_printable("Failed to deserialize the public key")?; + let apple_pay_ppc_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = + external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = + masking::Secret::new(state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone()) + .fetch_inner::(client) + .await + .change_context(errors::ApplePayDecryptionError::DecryptionFailed) + .attach_printable("Failed while creating client")? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.applepay_decrypt_keys.apple_pay_ppc_key.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let decrypted_apple_pay_ppc_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(&state.conf.applepay_decrypt_keys.apple_pay_ppc_key) + .decrypt(&apple_pay_ppc_key) .await .change_context(errors::ApplePayDecryptionError::DecryptionFailed)?; #[cfg(not(feature = "kms"))] - let decrypted_apple_pay_ppc_key = &state.conf.applepay_decrypt_keys.apple_pay_ppc_key; + let decrypted_apple_pay_ppc_key = &apple_pay_ppc_key; // Create PKey objects from EcKey let private_key = PKey::private_key_from_pem(decrypted_apple_pay_ppc_key.as_bytes()) .into_report() diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index 0750ff82bf5e..d805925f3183 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -5,6 +5,8 @@ use api_models::{ payment_methods::{self, BankAccountAccessCreds}, payments::{AddressDetails, BankDebitBilling, BankDebitData, PaymentMethodData}, }; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch}; use hex; pub mod helpers; pub mod transformers; @@ -345,15 +347,36 @@ async fn store_bank_details_in_payment_methods( } } + let pm_auth_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = external_services::hashicorp_vault::get_hashicorp_client(&state.conf.hc_vault) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed while creating client")?; + + #[cfg(feature = "hashicorp-vault")] + let output = masking::Secret::new(state.conf.payment_method_auth.pm_auth_key.clone()) + .fetch_inner::(client) + .await + .change_context(ApiErrorResponse::InternalServerError)? + .expose(); + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.payment_method_auth.pm_auth_key.clone(); + + Ok::<_, error_stack::Report>(output) + } + .await?; + #[cfg(feature = "kms")] let pm_auth_key = kms::get_kms_client(&state.conf.kms) .await - .decrypt(state.conf.payment_method_auth.pm_auth_key.clone()) + .decrypt(pm_auth_key) .await .change_context(ApiErrorResponse::InternalServerError)?; #[cfg(not(feature = "kms"))] - let pm_auth_key = state.conf.payment_method_auth.pm_auth_key.clone(); + let pm_auth_key = pm_auth_key; let mut update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)> = Vec::new(); diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 9293d6e11431..fb1851af00d9 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -1,4 +1,6 @@ use actix_web::{web, HttpRequest, Responder}; +#[cfg(feature = "hashicorp-vault")] +use error_stack::ResultExt; use router_env::{instrument, tracing, Flow}; use super::app::AppState; @@ -44,10 +46,20 @@ pub async fn api_key_create( |state, _, payload| async { #[cfg(feature = "kms")] let kms_client = external_services::kms::get_kms_client(&state.clone().conf.kms).await; + + #[cfg(feature = "hashicorp-vault")] + let hc_client = external_services::hashicorp_vault::get_hashicorp_client( + &state.clone().conf.hc_vault, + ) + .await + .change_context(crate::core::errors::ApiErrorResponse::InternalServerError)?; + api_keys::create_api_key( state, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, payload, merchant_id.clone(), ) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 4345109a6724..d3a43f0f490d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1,10 +1,12 @@ use std::sync::Arc; use actix_web::{web, Scope}; -#[cfg(all(feature = "kms", feature = "olap"))] +#[cfg(all(feature = "olap", any(feature = "hashicorp-vault", feature = "kms")))] use analytics::AnalyticsConfig; #[cfg(feature = "email")] use external_services::email::{ses::AwsSes, EmailService}; +#[cfg(all(feature = "olap", feature = "hashicorp-vault"))] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; #[cfg(all(feature = "olap", feature = "kms"))] @@ -146,6 +148,12 @@ impl AppState { Box::pin(async move { #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&conf.kms).await; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + let hc_client = + external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + .await + .expect("Failed while creating hashicorp_client"); let testable = storage_impl == StorageImpl::PostgresqlTest; #[allow(clippy::expect_used)] let event_handler = conf @@ -153,6 +161,7 @@ impl AppState { .get_event_handler() .await .expect("Failed to create event handler"); + let store: Box = match storage_impl { StorageImpl::Postgresql | StorageImpl::PostgresqlTest => match &event_handler { EventsHandler::Kafka(kafka_client) => Box::new( @@ -180,6 +189,22 @@ impl AppState { ), }; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + match conf.analytics { + AnalyticsConfig::Clickhouse { .. } => {} + AnalyticsConfig::Sqlx { ref mut sqlx } + | AnalyticsConfig::CombinedCkh { ref mut sqlx, .. } + | AnalyticsConfig::CombinedSqlx { ref mut sqlx, .. } => { + sqlx.password = sqlx + .password + .clone() + .fetch_inner::(hc_client) + .await + .expect("Failed while fetching from hashicorp vault"); + } + }; + #[cfg(all(feature = "kms", feature = "olap"))] #[allow(clippy::expect_used)] match conf.analytics { @@ -195,6 +220,16 @@ impl AppState { } }; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.connector_onboarding = conf + .connector_onboarding + .fetch_inner::(hc_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(all(feature = "kms", feature = "olap"))] #[allow(clippy::expect_used)] { @@ -208,6 +243,17 @@ impl AppState { #[cfg(feature = "olap")] let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; + #[cfg(all(feature = "hashicorp-vault", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.jwekey = conf + .jwekey + .clone() + .fetch_inner::(hc_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(feature = "kms")] #[allow(clippy::expect_used)] let kms_secrets = settings::ActiveKmsSecrets { diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 8c973105d53b..c0ed2b442d0e 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -13,15 +13,15 @@ pub mod recon; #[cfg(feature = "email")] pub mod email; -#[cfg(feature = "kms")] +#[cfg(any(feature = "kms", feature = "hashicorp-vault"))] use data_models::errors::StorageError; use data_models::errors::StorageResult; use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; -#[cfg(not(feature = "kms"))] -use masking::PeekInterface; -use masking::StrongSecret; +use masking::{PeekInterface, StrongSecret}; #[cfg(feature = "kv_store")] use storage_impl::KVRouterStore; use storage_impl::RouterStore; @@ -48,39 +48,58 @@ pub async fn get_store( #[cfg(feature = "kms")] let kms_client = kms::get_kms_client(&config.kms).await; + #[cfg(feature = "hashicorp-vault")] + let hc_client = external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .change_context(StorageError::InitializationError)?; + + let master_config = config.master_database.clone(); + + #[cfg(feature = "hashicorp-vault")] + let master_config = master_config + .fetch_inner::(hc_client) + .await + .change_context(StorageError::InitializationError) + .attach_printable("Failed to fetch data from hashicorp vault")?; + #[cfg(feature = "kms")] - let master_config = config - .master_database - .clone() + let master_config = master_config .decrypt_inner(kms_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to decrypt master database config")?; - #[cfg(not(feature = "kms"))] - let master_config = config.master_database.clone().into(); + + #[cfg(feature = "olap")] + let replica_config = config.replica_database.clone(); + + #[cfg(all(feature = "olap", feature = "hashicorp-vault"))] + let replica_config = replica_config + .fetch_inner::(hc_client) + .await + .change_context(StorageError::InitializationError) + .attach_printable("Failed to fetch data from hashicorp vault")?; #[cfg(all(feature = "olap", feature = "kms"))] - let replica_config = config - .replica_database - .clone() + let replica_config = replica_config .decrypt_inner(kms_client) .await .change_context(StorageError::InitializationError) .attach_printable("Failed to decrypt replica database config")?; - #[cfg(all(feature = "olap", not(feature = "kms")))] - let replica_config = config.replica_database.clone().into(); - let master_enc_key = get_master_enc_key( config, #[cfg(feature = "kms")] kms_client, + #[cfg(feature = "hashicorp-vault")] + hc_client, ) .await; #[cfg(not(feature = "olap"))] - let conf = master_config; + let conf = master_config.into(); #[cfg(feature = "olap")] - let conf = (master_config, replica_config); + // this would get abstracted, for all cases + #[allow(clippy::useless_conversion)] + let conf = (master_config.into(), replica_config.into()); let store: RouterStore = if test_transaction { RouterStore::test_store(conf, &config.redis, master_enc_key).await? @@ -110,21 +129,26 @@ pub async fn get_store( async fn get_master_enc_key( conf: &crate::configs::settings::Settings, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, ) -> StrongSecret> { + let master_enc_key = conf.secrets.master_enc_key.clone(); + + #[cfg(feature = "hashicorp-vault")] + let master_enc_key = master_enc_key + .fetch_inner::(hc_client) + .await + .expect("Failed to fetch master enc key"); + #[cfg(feature = "kms")] - let master_enc_key = hex::decode( - conf.secrets - .master_enc_key - .clone() + let master_enc_key = masking::Secret::<_, masking::WithType>::new( + master_enc_key .decrypt_inner(kms_client) .await .expect("Failed to decrypt master enc key"), - ) - .expect("Failed to decode from hex"); + ); - #[cfg(not(feature = "kms"))] - let master_enc_key = - hex::decode(conf.secrets.master_enc_key.peek()).expect("Failed to decode from hex"); + let master_enc_key = hex::decode(master_enc_key.peek()).expect("Failed to decode from hex"); StrongSecret::new(master_enc_key) } diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index eaadc0d5c7be..7f1e078ad53d 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -3,9 +3,13 @@ use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_utils::date_time; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] use external_services::kms::{self, decrypt::KmsDecrypt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +#[cfg(feature = "hashicorp-vault")] +use masking::ExposeInterface; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; @@ -222,6 +226,10 @@ where &config.api_keys, #[cfg(feature = "kms")] kms::get_kms_client(&config.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&config.hc_vault) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, ) .await? }; @@ -281,9 +289,14 @@ static ADMIN_API_KEY: tokio::sync::OnceCell> = pub async fn get_admin_api_key( secrets: &settings::Secrets, #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + #[cfg(feature = "hashicorp-vault")] + hc_client: &external_services::hashicorp_vault::HashiCorpVault, ) -> RouterResult<&'static StrongSecret> { ADMIN_API_KEY .get_or_try_init(|| async { + #[cfg(not(feature = "kms"))] + let admin_api_key = secrets.admin_api_key.clone(); + #[cfg(feature = "kms")] let admin_api_key = secrets .kms_encrypted_admin_api_key @@ -292,8 +305,13 @@ pub async fn get_admin_api_key( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to KMS decrypt admin API key")?; - #[cfg(not(feature = "kms"))] - let admin_api_key = secrets.admin_api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let admin_api_key = masking::Secret::new(admin_api_key) + .fetch_inner::(hc_client) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to KMS decrypt admin API key")? + .expose(); Ok(StrongSecret::new(admin_api_key)) }) @@ -348,6 +366,11 @@ where &conf.secrets, #[cfg(feature = "kms")] kms::get_kms_client(&conf.kms).await, + #[cfg(feature = "hashicorp-vault")] + external_services::hashicorp_vault::get_hashicorp_client(&conf.hc_vault) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting admin api key")?, ) .await?; diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs index 118d9df28e22..a01f2520b6ac 100644 --- a/crates/router/src/utils/currency.rs +++ b/crates/router/src/utils/currency.rs @@ -4,6 +4,8 @@ use api_models::enums; use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt}; use currency_conversion::types::{CurrencyFactors, ExchangeRates}; use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "hashicorp-vault")] +use external_services::hashicorp_vault::{self, decrypt::VaultFetch}; #[cfg(feature = "kms")] use external_services::kms; use masking::PeekInterface; @@ -127,6 +129,8 @@ async fn waited_fetch_and_update_caches( local_fetch_retry_delay: u64, local_fetch_retry_count: u64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { for _n in 1..local_fetch_retry_count { sleep(Duration::from_millis(local_fetch_retry_delay)).await; @@ -149,6 +153,8 @@ async fn waited_fetch_and_update_caches( None, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -187,6 +193,8 @@ pub async fn get_forex_rates( local_fetch_retry_delay: u64, local_fetch_retry_count: u64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { if let Some(local_rates) = retrieve_forex_from_local().await { if local_rates.is_expired(call_delay) { @@ -197,6 +205,8 @@ pub async fn get_forex_rates( local_rates, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } else { @@ -212,6 +222,8 @@ pub async fn get_forex_rates( local_fetch_retry_count, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -223,6 +235,8 @@ async fn handler_local_no_data( _local_fetch_retry_delay: u64, _local_fetch_retry_count: u64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match retrieve_forex_from_redis(state).await { Ok(Some(data)) => { @@ -232,6 +246,8 @@ async fn handler_local_no_data( call_delay, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -242,6 +258,8 @@ async fn handler_local_no_data( None, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await?) } @@ -252,6 +270,8 @@ async fn handler_local_no_data( None, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await?) } @@ -262,6 +282,8 @@ async fn successive_fetch_and_save_forex( state: &AppState, stale_redis_data: Option, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match acquire_redis_lock(state).await { Ok(lock_acquired) => { @@ -272,6 +294,8 @@ async fn successive_fetch_and_save_forex( state, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await; match api_rates { @@ -283,6 +307,8 @@ async fn successive_fetch_and_save_forex( state, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await; match secondary_api_rates { @@ -326,6 +352,8 @@ async fn fallback_forex_redis_check( redis_data: FxExchangeRatesCacheEntry, call_delay: i64, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await { Some(redis_forex) => { @@ -341,6 +369,8 @@ async fn fallback_forex_redis_check( Some(redis_data), #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -352,6 +382,8 @@ async fn handler_local_expired( call_delay: i64, local_rates: FxExchangeRatesCacheEntry, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { match retrieve_forex_from_redis(state).await { Ok(redis_data) => { @@ -370,6 +402,8 @@ async fn handler_local_expired( Some(local_rates), #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -383,6 +417,8 @@ async fn handler_local_expired( Some(local_rates), #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await } @@ -392,16 +428,40 @@ async fn handler_local_expired( async fn fetch_forex_rates( state: &AppState, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> Result> { + let forex_api_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = hashicorp_vault::get_hashicorp_client(hc_config) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.forex_api.api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let output = state + .conf + .forex_api + .api_key + .clone() + .fetch_inner::(client) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + Ok::<_, error_stack::Report>(output) + } + .await?; #[cfg(feature = "kms")] let forex_api_key = kms::get_kms_client(kms_config) .await - .decrypt(state.conf.forex_api.api_key.peek()) + .decrypt(forex_api_key.peek()) .await .change_context(ForexCacheError::KmsDecryptionFailed)?; #[cfg(not(feature = "kms"))] - let forex_api_key = state.conf.forex_api.api_key.peek(); + let forex_api_key = forex_api_key.peek(); let forex_url: String = format!("{}{}{}", FOREX_BASE_URL, forex_api_key, FOREX_BASE_CURRENCY); let forex_request = services::RequestBuilder::new() @@ -457,16 +517,39 @@ async fn fetch_forex_rates( pub async fn fallback_fetch_forex_rates( state: &AppState, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { + let fallback_api_key = async { + #[cfg(feature = "hashicorp-vault")] + let client = hashicorp_vault::get_hashicorp_client(hc_config) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + #[cfg(not(feature = "hashicorp-vault"))] + let output = state.conf.forex_api.fallback_api_key.clone(); + #[cfg(feature = "hashicorp-vault")] + let output = state + .conf + .forex_api + .fallback_api_key + .clone() + .fetch_inner::(client) + .await + .change_context(ForexCacheError::KmsDecryptionFailed)?; + + Ok::<_, error_stack::Report>(output) + } + .await?; #[cfg(feature = "kms")] let fallback_forex_api_key = kms::get_kms_client(kms_config) .await - .decrypt(state.conf.forex_api.fallback_api_key.peek()) + .decrypt(fallback_api_key.peek()) .await .change_context(ForexCacheError::KmsDecryptionFailed)?; #[cfg(not(feature = "kms"))] - let fallback_forex_api_key = state.conf.forex_api.fallback_api_key.peek(); + let fallback_forex_api_key = fallback_api_key.peek(); let fallback_forex_url: String = format!("{}{}", FALLBACK_FOREX_BASE_URL, fallback_forex_api_key,); @@ -609,6 +692,8 @@ pub async fn convert_currency( to_currency: String, from_currency: String, #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, + #[cfg(feature = "hashicorp-vault")] + hc_config: &external_services::hashicorp_vault::HashiCorpVaultConfig, ) -> CustomResult { let rates = get_forex_rates( &state, @@ -617,6 +702,8 @@ pub async fn convert_currency( state.conf.forex_api.local_fetch_retry_count, #[cfg(feature = "kms")] kms_config, + #[cfg(feature = "hashicorp-vault")] + hc_config, ) .await .change_context(ForexCacheError::ApiError)?; diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index 8dca7942ab0a..579f4ef10e7c 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -22,10 +22,10 @@ serde_path_to_error = "0.1.14" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", default-features = false, features = ["formatting"] } tokio = { version = "1.35.1" } -tracing = { version = "=0.1.36" } +tracing = { version = "0.1.37" } tracing-actix-web = { version = "0.7.8", features = ["opentelemetry_0_19", "uuid_v7"], optional = true } tracing-appender = { version = "0.2.2" } -tracing-attributes = "=0.1.22" +tracing-attributes = "0.1.27" tracing-opentelemetry = { version = "0.19.0" } tracing-subscriber = { version = "0.3.17", default-features = true, features = ["env-filter", "json", "registry"] } vergen = { version = "8.2.1", optional = true, features = ["cargo", "git", "git2", "rustc"] } @@ -43,4 +43,4 @@ actix_web = ["tracing-actix-web"] log_custom_entries_to_extra = [] log_extra_implicit_fields = [] log_active_span_json = [] -payouts = [] \ No newline at end of file +payouts = [] From 3f343d36bff7ce8f73602a2391d205367d5581c7 Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:48:59 +0530 Subject: [PATCH 374/443] chore(ckh-source): updated ckh analytics source tables (#3397) Co-authored-by: Sampras lopes --- crates/analytics/src/clickhouse.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index f81c29c801c0..00ae3b6e3103 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -354,11 +354,11 @@ impl ToSql for PrimitiveDateTime { impl ToSql for AnalyticsCollection { fn to_sql(&self, _table_engine: &TableEngine) -> error_stack::Result { match self { - Self::Payment => Ok("payment_attempt_dist".to_string()), - Self::Refund => Ok("refund_dist".to_string()), - Self::SdkEvents => Ok("sdk_events_dist".to_string()), - Self::ApiEvents => Ok("api_audit_log".to_string()), - Self::PaymentIntent => Ok("payment_intents_dist".to_string()), + Self::Payment => Ok("payment_attempts".to_string()), + Self::Refund => Ok("refunds".to_string()), + Self::SdkEvents => Ok("sdk_events_audit".to_string()), + Self::ApiEvents => Ok("api_events_audit".to_string()), + Self::PaymentIntent => Ok("payment_intents".to_string()), Self::ConnectorEvents => Ok("connector_events_audit".to_string()), Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), } From 8a019f08acf74e04c3ae9c8790dd481301bdcfee Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:46:29 +0530 Subject: [PATCH 375/443] Refactor(compatibility): revert add multiuse mandates support in stripe compatibility (#3436) --- .../stripe/payment_intents/types.rs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 51f938d445c4..38007a3110d6 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -738,25 +738,9 @@ impl ForeignTryFrom<(Option, Option)> for Option Some(payments::MandateType::MultiUse(Some( - payments::MandateAmountData { - amount: mandate.amount.unwrap_or_default(), - currency, - start_date: mandate.start_date, - end_date: mandate.end_date, - metadata: None, - }, - ))), + StripeMandateType::MultiUse => Some(payments::MandateType::MultiUse(None)), }, - None => Some(api_models::payments::MandateType::MultiUse(Some( - payments::MandateAmountData { - amount: mandate.amount.unwrap_or_default(), - currency, - start_date: mandate.start_date, - end_date: mandate.end_date, - metadata: None, - }, - ))), + None => Some(api_models::payments::MandateType::MultiUse(None)), }, customer_acceptance: Some(payments::CustomerAcceptance { acceptance_type: payments::AcceptanceType::Online, From 4cd65a24f70fdef160eb2d87654f1e30538c3339 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:09:53 +0530 Subject: [PATCH 376/443] Refactor(Router): [Noon] revert adding new field max_amount to mandate request (#3435) --- .../router/src/connector/noon/transformers.rs | 59 +++++++------------ 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 81f3ab33e2f3..bbf284848b59 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -1,5 +1,5 @@ use common_utils::pii; -use error_stack::{IntoReport, ResultExt}; +use error_stack::ResultExt; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -7,7 +7,7 @@ use crate::{ connector::utils::{ self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, }, - core::{errors, mandate::MandateBehaviour}, + core::errors, services, types::{self, api, storage::enums, transformers::ForeignFrom, ErrorResponse}, utils, @@ -30,13 +30,11 @@ pub enum NoonSubscriptionType { } #[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] pub struct NoonSubscriptionData { #[serde(rename = "type")] subscription_type: NoonSubscriptionType, //Short description about the subscription. name: String, - max_amount: String, } #[derive(Debug, Serialize)] @@ -93,7 +91,7 @@ pub struct NoonSubscription { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct NoonCard { - name_on_card: Option>, + name_on_card: Secret, number_plain: cards::CardNumber, expiry_month: Secret, expiry_year: Secret, @@ -160,7 +158,7 @@ pub struct NoonPayPal { } #[derive(Debug, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "UPPERCASE")] +#[serde(tag = "type", content = "data")] pub enum NoonPaymentData { Card(NoonCard), Subscription(NoonSubscription), @@ -202,7 +200,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { _ => ( match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => Ok(NoonPaymentData::Card(NoonCard { - name_on_card: req_card.card_holder_name.clone(), + name_on_card: req_card + .card_holder_name + .clone() + .unwrap_or(Secret::new("".to_string())), number_plain: req_card.card_number.clone(), expiry_month: req_card.card_exp_month.clone(), expiry_year: req_card.get_expiry_year_4_digit(), @@ -295,11 +296,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { } }?, Some(item.request.currency), - Some(item.request.order_category.clone().ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "order_category", - }, - )?), + item.request.order_category.clone(), ), }; @@ -333,33 +330,17 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { }, }); - let subscription = item - .request - .get_setup_mandate_details() - .map(|mandate_data| { - let max_amount = match &mandate_data.mandate_type { - Some(data_models::mandates::MandateDataType::SingleUse(mandate)) - | Some(data_models::mandates::MandateDataType::MultiUse(Some(mandate))) => { - conn_utils::to_currency_base_unit(mandate.amount, mandate.currency) - } - _ => Err(errors::ConnectorError::MissingRequiredField { - field_name: "setup_future_usage.mandate_data.mandate_type", - }) - .into_report(), - }?; - - Ok::>( - NoonSubscriptionData { - subscription_type: NoonSubscriptionType::Unscheduled, - name: name.clone(), - max_amount, - }, - ) - }) - .transpose()?; - - let tokenize_c_c = subscription.is_some().then_some(true); - + let (subscription, tokenize_c_c) = + match item.request.setup_future_usage.is_some().then_some(( + NoonSubscriptionData { + subscription_type: NoonSubscriptionType::Unscheduled, + name: name.clone(), + }, + true, + )) { + Some((a, b)) => (Some(a), Some(b)), + None => (None, None), + }; let order = NoonOrder { amount: conn_utils::to_currency_base_unit(item.request.amount, item.request.currency)?, currency, From 61439533594f93287f15aaffb5d65ed12e77e7ec Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:54:55 +0000 Subject: [PATCH 377/443] chore(version): 2024.01.24.1 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af517a6a1153..3e300ccbb119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.24.1 + +### Features + +- **hashicorp:** Implement hashicorp secrets manager solution ([#3297](https://github.com/juspay/hyperswitch/pull/3297)) ([`629d546`](https://github.com/juspay/hyperswitch/commit/629d546aa7c774e86d609abec3b3ab5cf0d100a7)) + +### Refactors + +- **Router:** [Noon] revert adding new field max_amount to mandate request ([#3435](https://github.com/juspay/hyperswitch/pull/3435)) ([`4cd65a2`](https://github.com/juspay/hyperswitch/commit/4cd65a24f70fdef160eb2d87654f1e30538c3339)) +- **compatibility:** Revert add multiuse mandates support in stripe compatibility ([#3436](https://github.com/juspay/hyperswitch/pull/3436)) ([`8a019f0`](https://github.com/juspay/hyperswitch/commit/8a019f08acf74e04c3ae9c8790dd481301bdcfee)) + +### Miscellaneous Tasks + +- **ckh-source:** Updated ckh analytics source tables ([#3397](https://github.com/juspay/hyperswitch/pull/3397)) ([`3f343d3`](https://github.com/juspay/hyperswitch/commit/3f343d36bff7ce8f73602a2391d205367d5581c7)) + +**Full Changelog:** [`2024.01.24.0...2024.01.24.1`](https://github.com/juspay/hyperswitch/compare/2024.01.24.0...2024.01.24.1) + +- - - + ## 2024.01.24.0 ### Miscellaneous Tasks From 77c1bbb5a3fe3244cd988ac1260a4a31ae7fcd20 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:45:25 +0530 Subject: [PATCH 378/443] refactor(configs): add configs for deployments to environments (#3265) Co-authored-by: Sk Sakil Mostak --- config/config.example.toml | 113 ++++---- config/deployments/README.md | 158 +++++++++++ config/deployments/drainer.toml | 39 +++ config/deployments/env_specific.toml | 211 +++++++++++++++ config/deployments/integration_test.toml | 279 +++++++++++++++++++ config/deployments/production.toml | 295 ++++++++++++++++++++ config/deployments/sandbox.toml | 296 +++++++++++++++++++++ config/deployments/scheduler/consumer.toml | 11 + config/deployments/scheduler/producer.toml | 14 + 9 files changed, 1362 insertions(+), 54 deletions(-) create mode 100644 config/deployments/README.md create mode 100644 config/deployments/drainer.toml create mode 100644 config/deployments/env_specific.toml create mode 100644 config/deployments/integration_test.toml create mode 100644 config/deployments/production.toml create mode 100644 config/deployments/sandbox.toml create mode 100644 config/deployments/scheduler/consumer.toml create mode 100644 config/deployments/scheduler/producer.toml diff --git a/config/config.example.toml b/config/config.example.toml index cf25ef195a24..0ad50736e9ed 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -45,25 +45,25 @@ queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 clie [redis] host = "127.0.0.1" port = 6379 -pool_size = 5 # Number of connections to keep open -reconnect_max_attempts = 5 # Maximum number of reconnection attempts to make before failing. Set to 0 to retry forever. -reconnect_delay = 5 # Delay between reconnection attempts, in milliseconds -default_ttl = 300 # Default TTL for entries, in seconds -default_hash_ttl = 900 # Default TTL for hashes entries, in seconds -use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6) -stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options -auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. +pool_size = 5 # Number of connections to keep open +reconnect_max_attempts = 5 # Maximum number of reconnection attempts to make before failing. Set to 0 to retry forever. +reconnect_delay = 5 # Delay between reconnection attempts, in milliseconds +default_ttl = 300 # Default TTL for entries, in seconds +default_hash_ttl = 900 # Default TTL for hashes entries, in seconds +use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6) +stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options +auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. -max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. -default_command_timeout = 0 # An optional timeout to apply to all commands. -max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 0 # An optional timeout to apply to all commands. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. # This section provides configs for currency conversion api [forex_api] call_delay = 21600 # Api calls are made after every 6 hrs local_fetch_retry_count = 5 # Fetch from Local cache has retry count as 5 local_fetch_retry_delay = 1000 # Retry delay for checking write condition -api_timeout = 20000 # Api timeouts once it crosses 2000 ms +api_timeout = 20000 # Api timeouts once it crosses 20000 ms api_key = "YOUR API KEY HERE" # Api key for making request to foreign exchange Api fallback_api_key = "YOUR API KEY" # Api key for the fallback service redis_lock_timeout = 26000 # Redis remains write locked for 26000 ms once the acquire_redis_lock is called @@ -126,12 +126,11 @@ kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) cipher # PCI Compliant storage entity which stores payment method information # like card details [locker] -host = "" # Locker host -host_rs = "" # Rust Locker host -mock_locker = true # Emulate a locker locally using Postgres -basilisk_host = "" # Basilisk host -locker_signing_key_id = "1" # Key_id to sign basilisk hs locker -locker_enabled = true # Boolean to enable or disable saving cards in locker +host = "" # Locker host +host_rs = "" # Rust Locker host +mock_locker = true # Emulate a locker locally using Postgres +locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +locker_enabled = true # Boolean to enable or disable saving cards in locker [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response @@ -144,7 +143,6 @@ vault_encryption_key = "" # public key in pem format, corresponding privat rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs - # Refund configuration [refund] max_attempts = 10 # Number of refund attempts allowed @@ -336,7 +334,6 @@ active_email_client = "SES" # The currently active email client email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session. - #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } @@ -352,9 +349,9 @@ stripe = { payment_method = "bank_transfer" } nuvei = { payment_method = "card" } shift4 = { payment_method = "card" } bluesnap = { payment_method = "card" } -bankofamerica = {payment_method = "card"} -cybersource = {payment_method = "card"} -nmi = {payment_method = "card"} +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi = { payment_method = "card" } [dummy_connector] enabled = true # Whether dummy connector is enabled or not @@ -377,13 +374,13 @@ slack_invite_url = "https://www.example.com/" # Slack invite url for hyperswit discord_invite_url = "https://www.example.com/" # Discord invite url for hyperswitch [mandates.supported_payment_methods] -card.credit = { connector_list = "stripe,adyen,cybersource" } # Mandate supported payment method type and connector for card -wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets -pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later -bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_redirect.ideal = {connector_list = "stripe,adyen"} # Mandate supported payment method type and connector for bank_redirect +card.credit = { connector_list = "stripe,adyen,cybersource" } # Mandate supported payment method type and connector for card +wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets +pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later +bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_redirect.ideal = { connector_list = "stripe,adyen" } # Mandate supported payment method type and connector for bank_redirect # Required fields info used while listing the payment_method_data @@ -464,10 +461,27 @@ adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_ba supported_connectors = "braintree" [applepay_decrypt_keys] -apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate -apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve -apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate -apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" # Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" # Private key generated by Elliptic-curve prime256v1 curve. You can use `openssl ecparam -out private.key -name prime256v1 -genkey` to generate the private key +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" # Private key generated by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key + +[applepay_merchant_configs] +# Run below command to get common merchant identifier for applepay in shell +# +# CERT_PATH="path/to/certificate.pem" +# MERCHANT_ID=$(openssl x509 -in "$CERT_PATH" -noout -text | +# awk -v oid="1.2.840.113635.100.6.32" ' +# BEGIN { RS = "\n\n" } +# /X509v3 extensions/ { in_extension=1 } +# in_extension && /'"$oid"'/ { print $0; exit }' | +# grep -oE '\.@[A-F0-9]+' | sed 's/^\.@//' +# ) +# echo "Merchant ID: $MERCHANT_ID" +common_merchant_identifier = "APPLE_PAY_COMMON_MERCHANT_IDENTIFIER" # This can be obtained by decrypting the apple_pay_ppc_key as shown above in comments +merchant_cert = "APPLE_PAY_MERCHANT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +merchant_cert_key = "APPLE_PAY_MERCHANT_CERTIFICATE_KEY" # Private key generate by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key +applepay_endpoint = "https://apple-pay-gateway.apple.com/paymentservices/registerMerchant" # Apple pay gateway merchant endpoint [payment_link] sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" @@ -505,28 +519,19 @@ ttl = 900 enabled = true [paypal_onboarding] -client_id = "paypal_client_id" # Client ID for PayPal onboarding -client_secret = "paypal_secret_key" # Secret key for PayPal onboarding -partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding -enabled = true # Switch to enable or disable PayPal onboarding - -[frm] -enabled = true - -[paypal_onboarding] -client_id = "paypal_client_id" # Client ID for PayPal onboarding +client_id = "paypal_client_id" # Client ID for PayPal onboarding client_secret = "paypal_secret_key" # Secret key for PayPal onboarding -partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding -enabled = true # Switch to enable or disable PayPal onboarding +partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding +enabled = true # Switch to enable or disable PayPal onboarding [events] -source = "logs" # The event sink to push events supports kafka or logs (stdout) +source = "logs" # The event sink to push events supports kafka or logs (stdout) [events.kafka] -brokers = [] # Kafka broker urls for bootstrapping the client -intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events -attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events -refund_analytics_topic = "topic" # Kafka topic to be used for Refund events -api_logs_topic = "topic" # Kafka topic to be used for incoming api events -connector_logs_topic = "topic" # Kafka topic to be used for connector api events -outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events +brokers = [] # Kafka broker urls for bootstrapping the client +intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events +attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events +refund_analytics_topic = "topic" # Kafka topic to be used for Refund events +api_logs_topic = "topic" # Kafka topic to be used for incoming api events +connector_logs_topic = "topic" # Kafka topic to be used for connector api events +outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events diff --git a/config/deployments/README.md b/config/deployments/README.md new file mode 100644 index 000000000000..c807892f1e06 --- /dev/null +++ b/config/deployments/README.md @@ -0,0 +1,158 @@ +# Configs for deployments + +## Introduction + +This directory contains the configs for deployments of Hyperswitch in different hosted environments. + +Hyperswitch has **3** components namely, + +- router +- drainer +- scheduler + - consumer + - producer + +We maintain configs for the `router` component for 3 different environments, namely, + +- Integration Test +- Sandbox +- Production + +To learn about what "router", "drainer" and "scheduler" is, please refer to the [Hyperswitch architecture][architecture] documentation. + +### Tree structure + +```text +config/deployments # Root directory for the deployment configs +├── README.md # This file +├── drainer.toml # Config specific to drainer +├── env_specific.toml # Config for environment specific values which are meant to be sensitive (to be set by the user) +├── integration_test.toml # Config specific to integration_test environment +├── production.toml # Config specific to production environment +├── sandbox.toml # Config specific to sandbox environment +└── scheduler # Directory for scheduler configs + ├── consumer.toml # Config specific to consumer + └── producer.toml # Config specific to producer +``` + +## Router + +The `integration_test.toml`, `sandbox.toml`, and `production.toml` files are configuration files for the environments `integration_test`, `sandbox`, and `production`, respectively. These files maintain a 1:1 mapping with the environment names, and it is recommended to use the same name for the environment throughout this document. + +### Generating a Config File for the Router + +The `env_specific.toml` file contains values that are specific to the environment. This file is kept separate because the values in it are sensitive and are meant to be set by the user. The `env_specific.toml` file is merged with the `integration_test.toml`, `sandbox.toml`, or `production.toml` file to create the final configuration file for the router. + +For example, to build and deploy Hyperswitch in the **sandbox environment**, you can duplicate the `env_specific.toml` file and rename it as `sandbox_config.toml`. Then, update the values in the file with the proper values for the sandbox environment. + +The environment-specific `sandbox.toml` file, which contains the Hyperswitch recommended defaults, is merged with the `sandbox_config.toml` file to create the final configuration file called `sandbox_release.toml`. This file is marked as ready for deploying on the sandbox environment. + +1. Duplicate the `env_specific.toml` file and rename it as `sandbox_config.toml`: + + ```shell + cp config/deployments/env_specific.toml config/deployments/sandbox_config.toml + ``` + +2. Update the values in the `sandbox_config.toml` file with the proper values for the sandbox environment: + + ```shell + vi config/deployments/sandbox_config.toml + ``` + +3. To merge the files you can use `cat`: + + ```shell + cat config/deployments/sandbox.toml config/deployments/sandbox_config.toml > config/deployments/sandbox_release.toml + ``` + +> [!NOTE] +> You can refer to the [`config.example.toml`][config_example] file to understand the variables that used are in the `env_specific.toml` file. + +## Scheduler + +The scheduler has two components, namely `consumer` and `producer`. + +The `consumer.toml` and `producer.toml` files are the configuration files for the `consumer` and `producer`, respectively. These files contain the default values recommended by Hyperswitch. + +### Generating a Config File for the Scheduler + +Scheduler configuration files are built on top of the router configuration files. So, the `sandbox_release.toml` file is merged with the `consumer.toml` or `producer.toml` file to create the final configuration file for the scheduler. + +You can use `cat` to merge the files in the terminal. + +- Below is an example for consumer in sandbox environment: + + ```shell + cat config/deployments/scheduler/consumer.toml config/deployments/sandbox_release.toml > config/deployments/consumer_sandbox_release.toml + ``` + +- Below is an example for producer in sandbox environment: + + ```shell + cat config/deployments/scheduler/producer.toml config/deployments/sandbox_release.toml > config/deployments/producer_sandbox_release.toml + ``` + +## Drainer + +Drainer is an independent component, and hence, the drainer configs can be used directly provided that the user updates the `drainer.toml` file with proper values before using. + +## Running Hyperswitch through Docker Compose + +To run the router, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +### Application services +hyperswitch-server: + image: juspaydotin/hyperswitch-router:latest # This pulls the latest image from Docker Hub. If you wish to use a version without added features (like KMS), you can replace `latest` with `standalone`. However, please note that the standalone version is not recommended for production use. + command: /local/bin/router --config-path /local/config/deployments/sandbox_release.toml # <--- Change this to the config file that is generated for the environment. + ports: + - "8080:8080" + volumes: + - ./config:/local/config +``` + +To run the producer, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +hyperswitch-producer: + image: juspaydotin/hyperswitch-producer:latest + command: /local/bin/scheduler --config-path /local/config/deployments/producer_sandbox_release.toml # <--- Change this to the config file that is generated for the environment. + volumes: + - ./config:/local/config + environment: + - SCHEDULER_FLOW=producer +``` + +To run the consumer, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +hyperswitch-consumer: + image: juspaydotin/hyperswitch-consumer:latest + command: /local/bin/scheduler --config-path /local/config/deployments/consumer_sandbox_release.toml # <--- Change this to the config file that is generated for the environment + volumes: + - ./config:/local/config + environment: + - SCHEDULER_FLOW=consumer +``` + +To run the drainer, you can use the following snippet in the `docker-compose.yml` file: + +```yaml +hyperswitch-drainer: + image: juspaydotin/hyperswitch-drainer:latest + command: /local/bin/drainer --config-path /local/config/deployments/drainer.toml + volumes: + - ./config:/local/config +``` + +> [!NOTE] +> You can replace the term `sandbox` with the environment name that you are deploying to (e.g., `production`, `integration_test`, etc.) with respective changes (optional) and use the same steps to generate the final configuration file for the environment. + +You can verify that the server is up and running by hitting the health check endpoint: + +```shell +curl --head --request GET 'http://localhost:8080/health' +``` + +[architecture]: /docs/architecture.md +[config_example]: /config/config.example.toml diff --git a/config/deployments/drainer.toml b/config/deployments/drainer.toml new file mode 100644 index 000000000000..42c89cbfd584 --- /dev/null +++ b/config/deployments/drainer.toml @@ -0,0 +1,39 @@ +[drainer] +loop_interval = 500 +max_read_count = 100 +num_partitions = 64 +shutdown_interval = 1000 +stream_name = "drainer_stream" + +[kms] +key_id = "kms_key_id" +region = "kms_region" + +[log.console] +enabled = true +level = "DEBUG" +log_format = "json" + +[log.telemetry] +metrics_enabled = true +otel_exporter_otlp_endpoint = "http://localhost:4317" + +[master_database] +dbname = "master_database_name" +host = "localhost" +password = "master_database_password" +pool_size = 3 +port = 5432 +username = "username" + +[redis] +cluster_enabled = false +cluster_urls = ["redis.cluster.uri-1:8080", "redis.cluster.uri-2:4115"] # List of redis cluster urls +default_ttl = 300 +host = "localhost" +pool_size = 5 +port = 6379 +reconnect_delay = 5 +reconnect_max_attempts = 5 +stream_read_count = 1 +use_legacy_version = false diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml new file mode 100644 index 000000000000..354c320e8f58 --- /dev/null +++ b/config/deployments/env_specific.toml @@ -0,0 +1,211 @@ +# For explanantion of each config, please refer to the `config/config.example.toml` file + +[analytics.clickhouse] +username = "clickhouse_username" # Clickhouse username +password = "clickhouse_password" # Clickhouse password (optional) +host = "http://localhost:8123" # Clickhouse host in http(s)://: format +database_name = "clickhouse_db_name" # Clickhouse database name + +# Analytics configuration. +[analytics] +source = "sqlx" # The Analytics source/strategy to be used + +[analytics.sqlx] +username = "db_user" # Analytics DB Username +password = "db_pass" # Analytics DB Password +host = "localhost" # Analytics DB Host +port = 5432 # Analytics DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client + +[api_keys] +kms_encrypted_hash_key = "base64_encoded_ciphertext" # Base64-encoded (KMS encrypted) ciphertext of the API key hashing key +hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # API key hashing key. Only applicable when KMS is disabled. + +[applepay_decrypt_keys] +apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" # Payment Processing Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Payment Processing Certificate +apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" # Private key generated by Elliptic-curve prime256v1 curve. You can use `openssl ecparam -out private.key -name prime256v1 -genkey` to generate the private key +apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" # Private key generated by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key + +[applepay_merchant_configs] +common_merchant_identifier = "APPLE_PAY_COMMON_MERCHANT_IDENTIFIER" # Refer to config.example.toml to learn how you can generate this value +merchant_cert = "APPLE_PAY_MERCHANT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate +merchant_cert_key = "APPLE_PAY_MERCHANT_CERTIFICATE_KEY" # Private key generate by RSA:2048 algorithm. Refer Hyperswitch Docs (https://docs.hyperswitch.io/hyperswitch-cloud/payment-methods-setup/wallets/apple-pay/ios-application/) to generate the private key +applepay_endpoint = "https://apple-pay-gateway.apple.com/paymentservices/registerMerchant" # Apple pay gateway merchant endpoint + +[connector_onboarding.paypal] +enabled = true # boolean +client_id = "paypal_client_id" +client_secret = "paypal_client_secret" +partner_id = "paypal_partner_id" + +[connector_request_reference_id_config] +merchant_ids_send_payment_id_as_connector_request_id = ["merchant_id_1", "merchant_id_2", "etc.,"] + +# EmailClient configuration. Only applicable when the `email` feature flag is enabled. +[email] +sender_email = "example@example.com" # Sender email +aws_region = "" # AWS region used by AWS SES +base_url = "" # Dashboard base url used when adding links that should redirect to self, say https://app.hyperswitch.io for example +allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email +active_email_client = "SES" # The currently active email client + +# Configuration for aws ses, applicable when the active email client is SES +[email.aws_ses] +email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails +sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session. + +[events] +source = "logs" # The event sink to push events supports kafka or logs (stdout) + +[events.kafka] +brokers = [] # Kafka broker urls for bootstrapping the client +intent_analytics_topic = "topic" # Kafka topic to be used for PaymentIntent events +attempt_analytics_topic = "topic" # Kafka topic to be used for PaymentAttempt events +refund_analytics_topic = "topic" # Kafka topic to be used for Refund events +api_logs_topic = "topic" # Kafka topic to be used for incoming api events +connector_logs_topic = "topic" # Kafka topic to be used for connector api events +outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events + +[file_upload_config] +bucket_name = "bucket" +region = "bucket_region" + +# This section provides configs for currency conversion api +[forex_api] +call_delay = 21600 # Api calls are made after every 6 hrs +local_fetch_retry_count = 5 # Fetch from Local cache has retry count as 5 +local_fetch_retry_delay = 1000 # Retry delay for checking write condition +api_timeout = 20000 # Api timeouts once it crosses 20000 ms +api_key = "YOUR API KEY HERE" # Api key for making request to foreign exchange Api +fallback_api_key = "YOUR API KEY" # Api key for the fallback service +redis_lock_timeout = 26000 # Redis remains write locked for 26000 ms once the acquire_redis_lock is called + +[jwekey] # 3 priv/pub key pair +vault_encryption_key = "" # public key in pem format, corresponding private key in rust locker +rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker +vault_private_key = "" # private key in pem format, corresponding public key in rust locker + +# KMS configuration. Only applicable when the `kms` feature flag is enabled. +[kms] +key_id = "" # The AWS key ID used by the KMS SDK for decrypting data. +region = "" # The AWS region used by the KMS SDK for decrypting data. + +# Locker settings contain details for accessing a card locker, a +# PCI Compliant storage entity which stores payment method information +# like card details +[locker] +host = "" # Locker host +host_rs = "" # Rust Locker host +mock_locker = true # Emulate a locker locally using Postgres +locker_signing_key_id = "1" # Key_id to sign basilisk hs locker +locker_enabled = true # Boolean to enable or disable saving cards in locker +redis_temp_locker_encryption_key = "redis_temp_locker_encryption_key" # Encryption key for redis temp locker + +[log.console] +enabled = true +level = "DEBUG" +log_format = "json" + +[log.file] +enabled = false +level = "DEBUG" +log_format = "json" + +# Telemetry configuration for metrics and traces +[log.telemetry] +traces_enabled = false # boolean [true or false], whether traces are enabled +metrics_enabled = false # boolean [true or false], whether metrics are enabled +ignore_errors = false # boolean [true or false], whether to ignore errors during traces or metrics pipeline setup +sampling_rate = 0.1 # decimal rate between 0.0 - 1.0 +otel_exporter_otlp_endpoint = "http://localhost:4317" # endpoint to send metrics and traces to, can include port number +otel_exporter_otlp_timeout = 5000 # timeout (in milliseconds) for sending metrics and traces +use_xray_generator = false # Set this to true for AWS X-ray compatible traces +route_to_trace = ["*/confirm"] + +[lock_settings] +delay_between_retries_in_milliseconds = 500 # Delay between retries in milliseconds +redis_lock_expiry_seconds = 180 # Seconds before the redis lock expires + +# Main SQL data store credentials +[master_database] +username = "db_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client + +[payment_link] +sdk_url = "http://localhost:9090/0.16.7/v0/HyperLoader.js" + +[payment_method_auth] +pm_auth_key = "pm_auth_key" # Payment method auth key used for authorization +redis_expiry = 900 # Redis expiry time in milliseconds + +[proxy] +http_url = "http://proxy_http_url" # Outgoing proxy http URL to proxy the HTTP traffic +https_url = "https://proxy_https_url" # Outgoing proxy https URL to proxy the HTTPS traffic + +# Redis credentials +[redis] +host = "127.0.0.1" +port = 6379 +pool_size = 5 # Number of connections to keep open +reconnect_max_attempts = 5 # Maximum number of reconnection attempts to make before failing. Set to 0 to retry forever. +reconnect_delay = 5 # Delay between reconnection attempts, in milliseconds +default_ttl = 300 # Default TTL for entries, in seconds +default_hash_ttl = 900 # Default TTL for hashes entries, in seconds +use_legacy_version = false # RESP protocol for fred crate (set this to true if using RESPv2 or redis version < 6) +stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options +auto_pipeline = true # Whether or not the client should automatically pipeline commands across tasks when possible. +disable_auto_backpressure = false # Whether or not to disable the automatic backpressure features when pipelining is enabled. +max_in_flight_commands = 5000 # The maximum number of in-flight commands (per connection) before backpressure will be applied. +default_command_timeout = 0 # An optional timeout to apply to all commands. +max_feed_count = 200 # The maximum number of frames that will be fed to a socket before flushing. +cluster_enabled = true # boolean +cluster_urls = ["redis.cluster.uri-1:8080", "redis.cluster.uri-2:4115"] # List of redis cluster urls + +# Replica SQL data store credentials +[replica_database] +username = "replica_user" # DB Username +password = "db_pass" # DB Password. Use base-64 encoded kms encrypted value here when kms is enabled +host = "localhost" # DB Host +port = 5432 # DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds +queue_strategy = "Fifo" # Add the queue strategy used by the database bb8 client + +[report_download_config] +dispute_function = "report_download_config_dispute_function" # Config to download dispute report +payment_function = "report_download_config_payment_function" # Config to download payment report +refund_function = "report_download_config_refund_function" # Config to download refund report +region = "report_download_config_region" # Region of the bucket + +# This section provides some secret values. +[secrets] +master_enc_key = "sample_key" # Master Encryption key used to encrypt merchant wise encryption key. Should be 32-byte long. +admin_api_key = "test_admin" # admin API key for admin authentication. Only applicable when KMS is disabled. +kms_encrypted_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the admin_api_key. Only applicable when KMS is enabled. +jwt_secret = "secret" # JWT secret used for user authentication. Only applicable when KMS is disabled. +kms_encrypted_jwt_secret = "" # Base64-encoded (KMS encrypted) ciphertext of the jwt_secret. Only applicable when KMS is enabled. +recon_admin_api_key = "recon_test_admin" # recon_admin API key for recon authentication. Only applicable when KMS is disabled. +kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciphertext of the recon_admin_api_key. Only applicable when KMS is enabled + +# Server configuration +[server] +base_url = "https://server_base_url" +workers = 8 +port = 8080 +host = "127.0.0.1" +# This is the grace time (in seconds) given to the actix-server to stop the execution +# For more details: https://actix.rs/docs/server/#graceful-shutdown +shutdown_timeout = 30 +# HTTP Request body limit. Defaults to 32kB +request_body_limit = 32_768 diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml new file mode 100644 index 000000000000..4a858588b504 --- /dev/null +++ b/config/deployments/integration_test.toml @@ -0,0 +1,279 @@ +[bank_config] +eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" +eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +online_banking_poland.adyen.banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" +online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" +online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" +open_banking_uk.adyen.banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" +przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" + +[connectors] +aci.base_url = "https://eu-test.oppwa.com/" +adyen.base_url = "https://checkout-test.adyen.com/" +adyen.secondary_base_url = "https://pal-test.adyen.com/" +airwallex.base_url = "https://api-demo.airwallex.com/" +applepay.base_url = "https://apple-pay-gateway.apple.com/" +authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" +bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" +bitpay.base_url = "https://test.bitpay.com" +bluesnap.base_url = "https://sandbox.bluesnap.com/" +bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" +boku.base_url = "https://$-api4-stage.boku.com" +braintree.base_url = "https://api.sandbox.braintreegateway.com/" +braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" +checkout.base_url = "https://api.sandbox.checkout.com/" +coinbase.base_url = "https://api.commerce.coinbase.com" +cryptopay.base_url = "https://business-sandbox.cryptopay.me" +cybersource.base_url = "https://apitest.cybersource.com/" +dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" +fiserv.base_url = "https://cert.api.fiservapps.com/" +forte.base_url = "https://sandbox.forte.net/api/v3" +globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" +globepay.base_url = "https://pay.globepay.co/" +gocardless.base_url = "https://api-sandbox.gocardless.com" +helcim.base_url = "https://api.helcim.com/" +iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" +klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" +mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" +multisafepay.base_url = "https://testapi.multisafepay.com/" +nexinets.base_url = "https://apitest.payengine.de/v1" +nmi.base_url = "https://secure.nmi.com/" +noon.base_url = "https://api-test.noonpayments.com/" +noon.key_mode = "Test" +nuvei.base_url = "https://ppp-test.nuvei.com/" +opayo.base_url = "https://pi-test.sagepay.com/" +opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" +payme.base_url = "https://sandbox.payme.io/" +paypal.base_url = "https://api-m.sandbox.paypal.com/" +payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" +powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" +rapyd.base_url = "https://sandboxapi.rapyd.net" +shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" +riskified.base_url = "https://sandbox.riskified.com/api" +square.base_url = "https://connect.squareupsandbox.com/" +square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" +stax.base_url = "https://apiprod.fattlabs.com/" +stripe.base_url = "https://api.stripe.com/" +stripe.base_url_file_upload = "https://files.stripe.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +tsys.base_url = "https://stagegw.transnox.com/" +volt.base_url = "https://api.sandbox.volt.io/" +wise.base_url = "https://api.sandbox.transferwise.tech/" +worldline.base_url = "https://eu.sandbox.api-ingenico.com/" +worldpay.base_url = "https://try.access.worldpay.com/" +zen.base_url = "https://api.zen-test.com/" +zen.secondary_base_url = "https://secure.zen-test.com/" + +[dummy_connector] +enabled = true +assets_base_url = "https://app.hyperswitch.io/assets/TestProcessor/" +authorize_ttl = 36000 +default_return_url = "https://app.hyperswitch.io/" +discord_invite_url = "https://discord.gg/wJZ7DVW8mm" +payment_complete_duration = 500 +payment_complete_tolerance = 100 +payment_duration = 1000 +payment_retrieve_duration = 500 +payment_retrieve_tolerance = 100 +payment_tolerance = 100 +payment_ttl = 172800 +refund_duration = 1000 +refund_retrieve_duration = 500 +refund_retrieve_tolerance = 100 +refund_tolerance = 100 +refund_ttl = 172800 +slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg" + +[frm] +enabled = true + +[connector_customer] +connector_list = "gocardless,stax,stripe" +payout_connector_list = "wise" + +[delayed_session_response] +connectors_with_delayed_session_response = "trustpay,payme" + +[mandates.supported_payment_methods] +bank_debit.ach.connector_list = "gocardless" +bank_debit.becs.connector_list = "gocardless" +bank_debit.sepa.connector_list = "gocardless" +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource" +wallet.google_pay.connector_list = "stripe,adyen,cybersource" +wallet.paypal.connector_list = "adyen" + +[multiple_api_version_supported_connectors] +supported_connectors = "braintree" + +[payouts] +payout_eligibility = true + +[pm_filters.default] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "GBP" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US,CA", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +paypal.country = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } + +[pm_filters.adyen] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +alfamart = { country = "ID", currency = "IDR" } +ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay_hk = { country = "HK", currency = "HKD" } +alma = { country = "FR", currency = "EUR" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,UK,SE,NO,AK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +bacs = { country = "UK", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bizum = { country = "ES", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +bni_va = { country = "ID", currency = "IDR" } +boleto = { country = "BR", currency = "BRL" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +dana = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +eps = { country = "AT", currency = "EUR" } +family_mart = { country = "JP", currency = "JPY" } +gcash = { country = "PH", currency = "PHP" } +giropay = { country = "DE", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +indomaret = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +lawson = { country = "JP", currency = "JPY" } +mandiri_va = { country = "ID", currency = "IDR" } +mb_way = { country = "PT", currency = "EUR" } +mini_stop = { country = "JP", currency = "JPY" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +momo = { country = "VN", currency = "VND" } +momo_atm = { country = "VN", currency = "VND" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +online_banking_thailand = { country = "TH", currency = "THB" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_bright = { country = "CA", currency = "CAD" } +pay_easy = { country = "JP", currency = "JPY" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +seicomart = { country = "JP", currency = "JPY" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +seven_eleven = { country = "JP", currency = "JPY" } +sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +swish = { country = "SE", currency = "SEK" } +touch_n_go = { country = "MY", currency = "MYR" } +trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +twint = { country = "CH", currency = "CHF" } +vipps = { country = "NO", currency = "NOK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD,CNY" } + +[pm_filters.authorizedotnet] +google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" +paypal.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" + +[pm_filters.braintree] +paypal.currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" + +[pm_filters.forte] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.helcim] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.globepay] +ali_pay.currency = "GBP,CNY" +we_chat_pay.currency = "GBP,CNY" + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.prophetpay] +card_redirect.currency = "USD" + +[pm_filters.stax] +ach = { country = "US", currency = "USD" } + +[pm_filters.stripe] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } + +[pm_filters.worldpay] +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" + +[pm_filters.zen] +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pix = { country = "BR", currency = "BRL" } +pse = { country = "CO", currency = "COP" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + +[temp_locker_enable_config] +bluesnap.payment_method = "card" +nuvei.payment_method = "card" +shift4.payment_method = "card" +stripe.payment_method = "bank_transfer" +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi.payment_method = "card" + +[tokenization] +braintree = { long_lived_token = false, payment_method = "card" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } +mollie = { long_lived_token = false, payment_method = "card" } +payme = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } +stax = { long_lived_token = true, payment_method = "card,bank_debit" } +stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } + +[webhooks] +outgoing_enabled = true + +[webhook_source_verification_call] +connectors_with_webhook_source_verification_call = "paypal" diff --git a/config/deployments/production.toml b/config/deployments/production.toml new file mode 100644 index 000000000000..376ae579a507 --- /dev/null +++ b/config/deployments/production.toml @@ -0,0 +1,295 @@ +[bank_config] +eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" +eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +online_banking_poland.adyen.banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" +online_banking_slovakia.adyen.banks = "e_platby_v_u_b,e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo,volksbank_gruppe,volkskredit_bank_ag,vr_bank_braunau" +online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" +open_banking_uk.adyen.banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" +przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" + +[connector_customer] +connector_list = "stax,stripe,gocardless" +payout_connector_list = "wise" + +[connectors] +aci.base_url = "https://eu-test.oppwa.com/" +adyen.base_url = "https://checkout-test.adyen.com/" +adyen.secondary_base_url = "https://pal-test.adyen.com/" +airwallex.base_url = "https://api-demo.airwallex.com/" +applepay.base_url = "https://apple-pay-gateway.apple.com/" +authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" +bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://api.merchant-services.bankofamerica.com/" +bitpay.base_url = "https://bitpay.com" +bluesnap.base_url = "https://ws.bluesnap.com/" +bluesnap.secondary_base_url = "https://pay.bluesnap.com/" +boku.base_url = "https://country-api4-stage.boku.com" +braintree.base_url = "https://api.sandbox.braintreegateway.com/" +braintree.secondary_base_url = "https://payments.braintree-api.com/graphql" +cashtocode.base_url = "https://cluster14.api.cashtocode.com" +checkout.base_url = "https://api.checkout.com/" +coinbase.base_url = "https://api.commerce.coinbase.com" +cryptopay.base_url = "https://business.cryptopay.me/" +cybersource.base_url = "https://api.cybersource.com/" +dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" +fiserv.base_url = "https://cert.api.fiservapps.com/" +forte.base_url = "https://sandbox.forte.net/api/v3" +globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" +globepay.base_url = "https://pay.globepay.co/" +gocardless.base_url = "https://api.gocardless.com" +helcim.base_url = "https://api.helcim.com/" +iatapay.base_url = "https://iata-pay.iata.org/api/v1" +klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" +mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" +multisafepay.base_url = "https://testapi.multisafepay.com/" +nexinets.base_url = "https://api.payengine.de/v1" +nmi.base_url = "https://secure.nmi.com/" +noon.base_url = "https://api.noonpayments.com/" +noon.key_mode = "Live" +nuvei.base_url = "https://ppp-test.nuvei.com/" +opayo.base_url = "https://pi-live.sagepay.com/" +opennode.base_url = "https://api.opennode.com" +payeezy.base_url = "https://api.payeezy.com/" +payme.base_url = "https://live.payme.io/" +paypal.base_url = "https://api-m.paypal.com/" +payu.base_url = "https://secure.payu.com/api/" +placetopay.base_url = "https://checkout.placetopay.com/rest/gateway" +powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" +rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://wh.riskified.com/api/" +shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" +square.base_url = "https://connect.squareupsandbox.com/" +square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" +stax.base_url = "https://apiprod.fattlabs.com/" +stripe.base_url = "https://api.stripe.com/" +stripe.base_url_file_upload = "https://files.stripe.com/" +trustpay.base_url = "https://tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +tsys.base_url = "https://gateway.transit-pass.com/" +volt.base_url = "https://api.volt.io/" +wise.base_url = "https://api.sandbox.transferwise.tech/" +worldline.base_url = "https://eu.sandbox.api-ingenico.com/" +worldpay.base_url = "https://try.access.worldpay.com/" +zen.base_url = "https://api.zen.com/" +zen.secondary_base_url = "https://secure.zen.com/" + +[delayed_session_response] +connectors_with_delayed_session_response = "trustpay,payme" + +[dummy_connector] +assets_base_url = "https://app.hyperswitch.io/assets/TestProcessor/" +authorize_ttl = 36000 +default_return_url = "https://app.hyperswitch.io/" +discord_invite_url = "https://discord.gg/wJZ7DVW8mm" +enabled = false +payment_complete_duration = 500 +payment_complete_tolerance = 100 +payment_duration = 1000 +payment_retrieve_duration = 500 +payment_retrieve_tolerance = 100 +payment_tolerance = 100 +payment_ttl = 172800 +refund_duration = 1000 +refund_retrieve_duration = 500 +refund_retrieve_tolerance = 100 +refund_tolerance = 100 +refund_ttl = 172800 +slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg" + +[frm] +enabled = false + +[mandates.supported_payment_methods] +bank_debit.ach.connector_list = "gocardless" +bank_debit.becs.connector_list = "gocardless" +bank_debit.sepa.connector_list = "gocardless" +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource" +wallet.google_pay.connector_list = "stripe,adyen,cybersource" +wallet.paypal.connector_list = "adyen" + +[multiple_api_version_supported_connectors] +supported_connectors = "braintree" + +[payouts] +payout_eligibility = true + +[pm_filters.default] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD,CNY" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +bacs = { country = "UK", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +mb_way = { country = "PT", currency = "EUR" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +pay_bright = { country = "CA", currency = "CAD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.adyen] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,CA,ES,FR,IT,NZ,UK,US", currency = "USD,AUD,CAD,NZD,GBP" } +alfamart = { country = "ID", currency = "IDR" } +ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay_hk = { country = "HK", currency = "HKD" } +alma = { country = "FR", currency = "EUR" } +apple_pay = { country = "AE,AK,AM,AR,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CN,CO,CR,CY,CZ,DE,DK,EE,ES,FI,FO,FR,GB,GE,GG,GL,GR,HK,HR,HU,IE,IL,IM,IS,IT,JE,JO,JP,KW,KZ,LI,LT,LU,LV,MC,MD,ME,MO,MT,MX,MY,NL,NO,NZ,PE,PL,PS,PT,QA,RO,RS,SA,SE,SG,SI,SK,SM,TW,UA,UK,UM,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +bacs = { country = "UK", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bizum = { country = "ES", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +bni_va = { country = "ID", currency = "IDR" } +boleto = { country = "BR", currency = "BRL" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +dana = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +eps = { country = "AT", currency = "EUR" } +family_mart = { country = "JP", currency = "JPY" } +gcash = { country = "PH", currency = "PHP" } +giropay = { country = "DE", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +google_pay = { country = "AE,AG,AL,AO,AR,AS,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CL,CO,CY,CZ,DE,DK,DO,DZ,EE,EG,ES,FI,FR,GB,GR,HK,HR,HU,ID,IE,IL,IN,IS,IT,JO,JP,KE,KW,KZ,LB,LI,LK,LT,LU,LV,MT,MX,MY,NL,NO,NZ,OM,PA,PE,PH,PK,PL,PT,QA,RO,RU,SA,SE,SG,SI,SK,TH,TR,TW,UA,UK,US,UY,VN,ZA", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +indomaret = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +klarna = { country = "AT,BE,CA,CH,DE,DK,ES,FI,FR,GB,IE,IT,NL,NO,PL,PT,SE,UK,US", currency = "AUD,CAD,CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD" } +lawson = { country = "JP", currency = "JPY" } +mandiri_va = { country = "ID", currency = "IDR" } +mb_way = { country = "PT", currency = "EUR" } +mini_stop = { country = "JP", currency = "JPY" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +momo = { country = "VN", currency = "VND" } +momo_atm = { country = "VN", currency = "VND" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +online_banking_thailand = { country = "TH", currency = "THB" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_bright = { country = "CA", currency = "CAD" } +pay_easy = { country = "JP", currency = "JPY" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +seicomart = { country = "JP", currency = "JPY" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +seven_eleven = { country = "JP", currency = "JPY" } +sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE,UK", currency = "EUR" } +swish = { country = "SE", currency = "SEK" } +touch_n_go = { country = "MY", currency = "MYR" } +trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +twint = { country = "CH", currency = "CHF" } +vipps = { country = "NO", currency = "NOK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.authorizedotnet] +google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" +paypal.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" + +[pm_filters.braintree] +paypal.currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" + +[pm_filters.forte] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.helcim] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.globepay] +ali_pay.currency = "GBP,CNY" +we_chat_pay.currency = "GBP,CNY" + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.prophetpay] +card_redirect.currency = "USD" + +[pm_filters.stax] +ach = { country = "US", currency = "USD" } + +[pm_filters.stripe] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } + +[pm_filters.worldpay] +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" + +[pm_filters.zen] +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pix = { country = "BR", currency = "BRL" } +pse = { country = "CO", currency = "COP" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + +[temp_locker_enable_config] +bluesnap.payment_method = "card" +nuvei.payment_method = "card" +shift4.payment_method = "card" +stripe.payment_method = "bank_transfer" +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi.payment_method = "card" + +[tokenization] +braintree = { long_lived_token = false, payment_method = "card" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } +mollie = { long_lived_token = false, payment_method = "card" } +payme = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } +stax = { long_lived_token = true, payment_method = "card,bank_debit" } +stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } + +[webhooks] +outgoing_enabled = true + +[webhook_source_verification_call] +connectors_with_webhook_source_verification_call = "paypal" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml new file mode 100644 index 000000000000..01616f3ecd08 --- /dev/null +++ b/config/deployments/sandbox.toml @@ -0,0 +1,296 @@ +[bank_config] +eps.adyen.banks = "bank_austria,bawag_psk_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_tirol_bank_ag,posojilnica_bank_e_gen,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag" +eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" +ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" +online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +online_banking_poland.adyen.banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" +online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" +online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" +open_banking_uk.adyen.banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" +przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" + +[connector_customer] +connector_list = "stax,stripe,gocardless" +payout_connector_list = "wise" + +[connectors] +aci.base_url = "https://eu-test.oppwa.com/" +adyen.base_url = "https://checkout-test.adyen.com/" +adyen.secondary_base_url = "https://pal-test.adyen.com/" +airwallex.base_url = "https://api-demo.airwallex.com/" +applepay.base_url = "https://apple-pay-gateway.apple.com/" +authorizedotnet.base_url = "https://apitest.authorize.net/xml/v1/request.api" +bambora.base_url = "https://api.na.bambora.com" +bankofamerica.base_url = "https://apitest.merchant-services.bankofamerica.com/" +bitpay.base_url = "https://test.bitpay.com" +bluesnap.base_url = "https://sandbox.bluesnap.com/" +bluesnap.secondary_base_url = "https://sandpay.bluesnap.com/" +boku.base_url = "https://$-api4-stage.boku.com" +braintree.base_url = "https://api.sandbox.braintreegateway.com/" +braintree.secondary_base_url = "https://payments.sandbox.braintree-api.com/graphql" +cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" +checkout.base_url = "https://api.sandbox.checkout.com/" +coinbase.base_url = "https://api.commerce.coinbase.com" +cryptopay.base_url = "https://business-sandbox.cryptopay.me" +cybersource.base_url = "https://apitest.cybersource.com/" +dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" +fiserv.base_url = "https://cert.api.fiservapps.com/" +forte.base_url = "https://sandbox.forte.net/api/v3" +globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" +globepay.base_url = "https://pay.globepay.co/" +gocardless.base_url = "https://api-sandbox.gocardless.com" +helcim.base_url = "https://api.helcim.com/" +iatapay.base_url = "https://iata-pay.iata.org/api/v1" +klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" +mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" +multisafepay.base_url = "https://testapi.multisafepay.com/" +nexinets.base_url = "https://apitest.payengine.de/v1" +nmi.base_url = "https://secure.nmi.com/" +noon.base_url = "https://api-test.noonpayments.com/" +noon.key_mode = "Test" +nuvei.base_url = "https://ppp-test.nuvei.com/" +opayo.base_url = "https://pi-test.sagepay.com/" +opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" +payme.base_url = "https://sandbox.payme.io/" +paypal.base_url = "https://api-m.sandbox.paypal.com/" +payu.base_url = "https://secure.snd.payu.com/" +placetopay.base_url = "https://test.placetopay.com/rest/gateway" +powertranz.base_url = "https://staging.ptranz.com/api/" +prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" +rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" +shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" +square.base_url = "https://connect.squareupsandbox.com/" +square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" +stax.base_url = "https://apiprod.fattlabs.com/" +stripe.base_url = "https://api.stripe.com/" +stripe.base_url_file_upload = "https://files.stripe.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +tsys.base_url = "https://stagegw.transnox.com/" +volt.base_url = "https://api.sandbox.volt.io/" +wise.base_url = "https://api.sandbox.transferwise.tech/" +worldline.base_url = "https://eu.sandbox.api-ingenico.com/" +worldpay.base_url = "https://try.access.worldpay.com/" +zen.base_url = "https://api.zen-test.com/" +zen.secondary_base_url = "https://secure.zen-test.com/" + +[delayed_session_response] +connectors_with_delayed_session_response = "trustpay,payme" + +[dummy_connector] +enabled = true +assets_base_url = "https://app.hyperswitch.io/assets/TestProcessor/" +authorize_ttl = 36000 +default_return_url = "https://app.hyperswitch.io/" +discord_invite_url = "https://discord.gg/wJZ7DVW8mm" +payment_complete_duration = 500 +payment_complete_tolerance = 100 +payment_duration = 1000 +payment_retrieve_duration = 500 +payment_retrieve_tolerance = 100 +payment_tolerance = 100 +payment_ttl = 172800 +refund_duration = 1000 +refund_retrieve_duration = 500 +refund_retrieve_tolerance = 100 +refund_tolerance = 100 +refund_ttl = 172800 +slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-1k6cz4lee-SAJzhz6bjmpp4jZCDOtOIg" + +[frm] +enabled = true + +[mandates.supported_payment_methods] +bank_debit.ach.connector_list = "gocardless" +bank_debit.becs.connector_list = "gocardless" +bank_debit.sepa.connector_list = "gocardless" +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource" +wallet.google_pay.connector_list = "stripe,adyen,cybersource" +wallet.paypal.connector_list = "adyen" + +[multiple_api_version_supported_connectors] +supported_connectors = "braintree" + +[payouts] +payout_eligibility = true + +[pm_filters.default] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +bacs = { country = "UK", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +mb_way = { country = "PT", currency = "EUR" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +pay_bright = { country = "CA", currency = "CAD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.adyen] +ach = { country = "US", currency = "USD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +alfamart = { country = "ID", currency = "IDR" } +ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay_hk = { country = "HK", currency = "HKD" } +alma = { country = "FR", currency = "EUR" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,UK,SE,NO,AK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +atome = { country = "MY,SG", currency = "MYR,SGD" } +bacs = { country = "UK", currency = "GBP" } +bancontact_card = { country = "BE", currency = "EUR" } +bca_bank_transfer = { country = "ID", currency = "IDR" } +bizum = { country = "ES", currency = "EUR" } +blik = { country = "PL", currency = "PLN" } +bni_va = { country = "ID", currency = "IDR" } +boleto = { country = "BR", currency = "BRL" } +bri_va = { country = "ID", currency = "IDR" } +cimb_va = { country = "ID", currency = "IDR" } +dana = { country = "ID", currency = "IDR" } +danamon_va = { country = "ID", currency = "IDR" } +eps = { country = "AT", currency = "EUR" } +family_mart = { country = "JP", currency = "JPY" } +gcash = { country = "PH", currency = "PHP" } +giropay = { country = "DE", currency = "EUR" } +go_pay = { country = "ID", currency = "IDR" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +ideal = { country = "NL", currency = "EUR" } +indomaret = { country = "ID", currency = "IDR" } +kakao_pay = { country = "KR", currency = "KRW" } +klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +lawson = { country = "JP", currency = "JPY" } +mandiri_va = { country = "ID", currency = "IDR" } +mb_way = { country = "PT", currency = "EUR" } +mini_stop = { country = "JP", currency = "JPY" } +mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } +momo = { country = "VN", currency = "VND" } +momo_atm = { country = "VN", currency = "VND" } +online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } +online_banking_finland = { country = "FI", currency = "EUR" } +online_banking_fpx = { country = "MY", currency = "MYR" } +online_banking_poland = { country = "PL", currency = "PLN" } +online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } +online_banking_thailand = { country = "TH", currency = "THB" } +open_banking_uk = { country = "GB", currency = "GBP" } +oxxo = { country = "MX", currency = "MXN" } +pay_bright = { country = "CA", currency = "CAD" } +pay_easy = { country = "JP", currency = "JPY" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +permata_bank_transfer = { country = "ID", currency = "IDR" } +seicomart = { country = "JP", currency = "JPY" } +sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } +seven_eleven = { country = "JP", currency = "JPY" } +sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +swish = { country = "SE", currency = "SEK" } +touch_n_go = { country = "MY", currency = "MYR" } +trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +twint = { country = "CH", currency = "CHF" } +vipps = { country = "NO", currency = "NOK" } +walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } + +[pm_filters.authorizedotnet] +google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" +paypal.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" + +[pm_filters.braintree] +paypal.currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" + +[pm_filters.forte] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.helcim] +credit.currency = "USD" +debit.currency = "USD" + +[pm_filters.globepay] +ali_pay.currency = "GBP,CNY" +we_chat_pay.currency = "GBP,CNY" + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.prophetpay] +card_redirect.currency = "USD" + +[pm_filters.stax] +ach = { country = "US", currency = "USD" } + +[pm_filters.stripe] +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD" } +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA" +cashapp = { country = "US", currency = "USD" } +eps = { country = "AT", currency = "EUR" } +giropay = { country = "DE", currency = "EUR" } +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" +ideal = { country = "NL", currency = "EUR" } +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,CAD,CHF,CZK,DKK,EUR,GBP,NOK,NZD,PLN,SEK,USD" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } + +[pm_filters.worldpay] +apple_pay.country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" +google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" + +[pm_filters.zen] +boleto = { country = "BR", currency = "BRL" } +efecty = { country = "CO", currency = "COP" } +multibanco = { country = "PT", currency = "EUR" } +pago_efectivo = { country = "PE", currency = "PEN" } +pix = { country = "BR", currency = "BRL" } +pse = { country = "CO", currency = "COP" } +red_compra = { country = "CL", currency = "CLP" } +red_pagos = { country = "UY", currency = "UYU" } + + +[temp_locker_enable_config] +bluesnap.payment_method = "card" +nuvei.payment_method = "card" +shift4.payment_method = "card" +stripe.payment_method = "bank_transfer" +bankofamerica = { payment_method = "card" } +cybersource = { payment_method = "card" } +nmi.payment_method = "card" + +[tokenization] +braintree = { long_lived_token = false, payment_method = "card" } +checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } +gocardless = { long_lived_token = true, payment_method = "bank_debit" } +mollie = { long_lived_token = false, payment_method = "card" } +payme = { long_lived_token = false, payment_method = "card" } +square = { long_lived_token = false, payment_method = "card" } +stax = { long_lived_token = true, payment_method = "card,bank_debit" } +stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { list = "google_pay", type = "disable_only" } } + +[webhooks] +outgoing_enabled = true + +[webhook_source_verification_call] +connectors_with_webhook_source_verification_call = "paypal" diff --git a/config/deployments/scheduler/consumer.toml b/config/deployments/scheduler/consumer.toml new file mode 100644 index 000000000000..907e3b8297e3 --- /dev/null +++ b/config/deployments/scheduler/consumer.toml @@ -0,0 +1,11 @@ +# Scheduler settings provides a point to modify the behaviour of scheduler flow. +# It defines the the streams/queues name and configuration as well as event selection variables +[scheduler] +consumer_group = "scheduler_group" +graceful_shutdown_interval = 60000 # Specifies how much time to wait while re-attempting shutdown for a service (in milliseconds) +loop_interval = 3000 # Specifies how much time to wait before starting the defined behaviour of producer or consumer (in milliseconds)0 +stream = "scheduler_stream" + +[scheduler.consumer] +consumer_group = "scheduler_group" +disabled = false # This flag decides if the consumer should actively consume task diff --git a/config/deployments/scheduler/producer.toml b/config/deployments/scheduler/producer.toml new file mode 100644 index 000000000000..579466a23cc8 --- /dev/null +++ b/config/deployments/scheduler/producer.toml @@ -0,0 +1,14 @@ +# Scheduler settings provides a point to modify the behaviour of scheduler flow. +# It defines the the streams/queues name and configuration as well as event selection variables +[scheduler] +consumer_group = "scheduler_group" +graceful_shutdown_interval = 60000 # Specifies how much time to wait while re-attempting shutdown for a service (in milliseconds) +loop_interval = 30000 # Specifies how much time to wait before starting the defined behaviour of producer or consumer (in milliseconds) +stream = "scheduler_stream" + +[scheduler.producer] +batch_size = 50 # Specifies the batch size the producer will push under a single entry in the redis queue +lock_key = "producer_locking_key" # The following keys defines the producer lock that is created in redis with +lock_ttl = 160 # the ttl being the expiry (in seconds) +lower_fetch_limit = 900 # Lower limit for fetching entries from redis queue (in seconds) +upper_fetch_limit = 0 # Upper limit for fetching entries from the redis queue (in seconds)0 From f1fd0b101791f20980e21e8fd8bf10fac3179209 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 00:20:31 +0000 Subject: [PATCH 379/443] chore(version): 2024.01.25.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e300ccbb119..071f60d224f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.25.0 + +### Refactors + +- **configs:** Add configs for deployments to environments ([#3265](https://github.com/juspay/hyperswitch/pull/3265)) ([`77c1bbb`](https://github.com/juspay/hyperswitch/commit/77c1bbb5a3fe3244cd988ac1260a4a31ae7fcd20)) + +**Full Changelog:** [`2024.01.24.1...2024.01.25.0`](https://github.com/juspay/hyperswitch/compare/2024.01.24.1...2024.01.25.0) + +- - - + ## 2024.01.24.1 ### Features From b45e4ca2a3788823701bdeac2e2a8c1147bb071a Mon Sep 17 00:00:00 2001 From: Jeeva Ramachandran <120017870+JeevaRamu0104@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:28:59 +0530 Subject: [PATCH 380/443] fix(euclid_wasm): include `payouts` feature in `default` features (#3392) --- crates/euclid_wasm/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index d9f5330a1b8d..13296bcde626 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true crate-type = ["cdylib"] [features] -default = ["connector_choice_bcompat", "connector_choice_mca_id"] +default = ["connector_choice_bcompat","payouts", "connector_choice_mca_id"] release = ["connector_choice_bcompat", "connector_choice_mca_id"] connector_choice_bcompat = ["api_models/connector_choice_bcompat"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] @@ -18,6 +18,7 @@ dummy_connector = ["kgraph_utils/dummy_connector", "connector_configs/dummy_conn production = ["connector_configs/production"] development = ["connector_configs/development"] sandbox = ["connector_configs/sandbox"] +payouts = [] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } From f0c7bb9a5228f2ee31858fea07abe4ecee9b78a2 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:35:55 +0530 Subject: [PATCH 381/443] refactor(connector): [Iatapay] refactor authorize flow and fix payment status mapping (#2409) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/router/src/connector/iatapay.rs | 95 +++++----- .../src/connector/iatapay/transformers.rs | 179 ++++++++++++++++-- 2 files changed, 203 insertions(+), 71 deletions(-) diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 0c156ef08b03..72d7b70b061d 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -369,15 +369,13 @@ impl ConnectorIntegration CustomResult { - let connector_id = req - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + let auth: iatapay::IatapayAuthType = + iatapay::IatapayAuthType::try_from(&req.connector_auth_type)?; + let merchant_id = auth.merchant_id.peek(); Ok(format!( - "{}/payments/{}", + "{}/merchants/{merchant_id}/payments/{}", self.base_url(connectors), - connector_id + req.connector_request_reference_id.clone() )) } @@ -634,23 +632,41 @@ impl api::IncomingWebhook for Iatapay { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let notif: IatapayPaymentsResponse = - request - .body - .parse_struct("IatapayPaymentsResponse") - .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; - if notif.iata_payment_id.is_some() { - Ok(api_models::webhooks::ObjectReferenceId::PaymentId( - api_models::payments::PaymentIdType::ConnectorTransactionId( - notif.iata_payment_id.unwrap_or_default(), - ), - )) - } else { - Ok(api_models::webhooks::ObjectReferenceId::RefundId( - api_models::webhooks::RefundIdType::ConnectorRefundId( - notif.iata_refund_id.unwrap_or_default(), - ), - )) + let notif: iatapay::IatapayWebhookResponse = request + .body + .parse_struct("IatapayWebhookResponse") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + match notif { + iatapay::IatapayWebhookResponse::IatapayPaymentWebhookBody(wh_body) => { + match wh_body.merchant_payment_id { + Some(merchant_payment_id) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId( + merchant_payment_id, + ), + )) + } + None => Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + wh_body.iata_payment_id, + ), + )), + } + } + iatapay::IatapayWebhookResponse::IatapayRefundWebhookBody(wh_body) => { + match wh_body.merchant_refund_id { + Some(merchant_refund_id) => { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::RefundId(merchant_refund_id), + )) + } + None => Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId( + wh_body.iata_refund_id, + ), + )), + } + } } } @@ -658,32 +674,11 @@ impl api::IncomingWebhook for Iatapay { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - let notif: IatapayPaymentsResponse = - request - .body - .parse_struct("IatapayPaymentsResponse") - .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - match notif.status { - iatapay::IatapayPaymentStatus::Authorized => match notif.iata_payment_id.is_some() { - true => Ok(api::IncomingWebhookEvent::PaymentIntentSuccess), - false => Ok(api::IncomingWebhookEvent::RefundSuccess), - }, - iatapay::IatapayPaymentStatus::Failed => match notif.iata_payment_id.is_some() { - true => Ok(api::IncomingWebhookEvent::PaymentIntentFailure), - false => Ok(api::IncomingWebhookEvent::RefundFailure), - }, - iatapay::IatapayPaymentStatus::Unknown - | iatapay::IatapayPaymentStatus::Created - | iatapay::IatapayPaymentStatus::Initiated - | iatapay::IatapayPaymentStatus::Cleared - | iatapay::IatapayPaymentStatus::Settled - | iatapay::IatapayPaymentStatus::Tobeinvestigated - | iatapay::IatapayPaymentStatus::Blocked - | iatapay::IatapayPaymentStatus::Locked - | iatapay::IatapayPaymentStatus::UnexpectedSettled => { - Ok(api::IncomingWebhookEvent::EventNotSupported) - } - } + let notif: iatapay::IatapayWebhookResponse = request + .body + .parse_struct("IatapayWebhookResponse") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + api::IncomingWebhookEvent::try_from(notif) } fn get_webhook_resource_object( diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index e6ecc6da2ffe..14b37d1418d1 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -6,7 +6,9 @@ use masking::{Secret, SwitchStrategy}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::{self, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData}, + connector::utils::{ + self as connector_util, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData, + }, consts, core::errors, services, @@ -45,7 +47,7 @@ impl { type Error = error_stack::Report; fn try_from( - (_currency_unit, _currency, _amount, item): ( + (currency_unit, currency, amount, item): ( &types::api::CurrencyUnit, types::storage::enums::Currency, i64, @@ -53,7 +55,7 @@ impl ), ) -> Result { Ok(Self { - amount: utils::to_currency_base_unit_asf64(_amount, _currency)?, + amount: connector_util::get_amount_as_f64(currency_unit, amount, currency)?, router_data: item, }) } @@ -136,7 +138,6 @@ impl let payment_method = item.router_data.payment_method; let country = match payment_method { PaymentMethod::Upi => "IN".to_string(), - PaymentMethod::Card | PaymentMethod::CardRedirect | PaymentMethod::PayLater @@ -154,7 +155,19 @@ impl api::PaymentMethodData::Upi(upi_data) => upi_data.vpa_id.map(|id| PayerInfo { token_id: id.switch_strategy(), }), - _ => None, + api::PaymentMethodData::Card(_) + | api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::Wallet(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => None, }; let payload = Self { merchant_id: IatapayAuthType::try_from(&item.router_data.connector_auth_type)? @@ -212,25 +225,21 @@ pub enum IatapayPaymentStatus { Initiated, Authorized, Settled, - Tobeinvestigated, - Blocked, Cleared, Failed, - Locked, #[serde(rename = "UNEXPECTED SETTLED")] UnexpectedSettled, - #[serde(other)] - Unknown, } impl From for enums::AttemptStatus { fn from(item: IatapayPaymentStatus) -> Self { match item { - IatapayPaymentStatus::Authorized | IatapayPaymentStatus::Settled => Self::Charged, + IatapayPaymentStatus::Authorized + | IatapayPaymentStatus::Settled + | IatapayPaymentStatus::Cleared => Self::Charged, IatapayPaymentStatus::Failed | IatapayPaymentStatus::UnexpectedSettled => Self::Failure, IatapayPaymentStatus::Created => Self::AuthenticationPending, IatapayPaymentStatus::Initiated => Self::Pending, - _ => Self::Voided, } } } @@ -276,7 +285,7 @@ fn get_iatpay_response( errors::ConnectorError, > { let status = enums::AttemptStatus::from(response.status); - let error = if status == enums::AttemptStatus::Failure { + let error = if connector_util::is_payment_failure(status) { Some(types::ErrorResponse { code: response .failure_code @@ -433,11 +442,32 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { + let refund_status = enums::RefundStatus::from(item.response.status); + let response = if connector_util::is_refund_failure(refund_status) { + Err(types::ErrorResponse { + code: item + .response + .failure_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .failure_details + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.failure_details, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.iata_refund_id.clone()), + }) + } else { + Ok(types::RefundsResponseData { connector_refund_id: item.response.iata_refund_id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), + refund_status, + }) + }; + + Ok(Self { + response, ..item.data }) } @@ -450,11 +480,31 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { + let refund_status = enums::RefundStatus::from(item.response.status); + let response = if connector_util::is_refund_failure(refund_status) { + Err(types::ErrorResponse { + code: item + .response + .failure_code + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + message: item + .response + .failure_details + .clone() + .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), + reason: item.response.failure_details, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: Some(item.response.iata_refund_id.clone()), + }) + } else { + Ok(types::RefundsResponseData { connector_refund_id: item.response.iata_refund_id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), + refund_status, + }) + }; + Ok(Self { + response, ..item.data }) } @@ -473,3 +523,90 @@ pub struct IatapayAccessTokenErrorResponse { pub error: String, pub path: String, } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IatapayPaymentWebhookBody { + pub status: IatapayWebhookStatus, + pub iata_payment_id: String, + pub merchant_payment_id: Option, + pub failure_code: Option, + pub failure_details: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IatapayRefundWebhookBody { + pub status: IatapayRefundWebhookStatus, + pub iata_refund_id: String, + pub merchant_refund_id: Option, + pub failure_code: Option, + pub failure_details: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum IatapayWebhookResponse { + IatapayPaymentWebhookBody(IatapayPaymentWebhookBody), + IatapayRefundWebhookBody(IatapayRefundWebhookBody), +} + +impl TryFrom for api::IncomingWebhookEvent { + type Error = error_stack::Report; + fn try_from(payload: IatapayWebhookResponse) -> CustomResult { + match payload { + IatapayWebhookResponse::IatapayPaymentWebhookBody(wh_body) => match wh_body.status { + IatapayWebhookStatus::Authorized | IatapayWebhookStatus::Settled => { + Ok(Self::PaymentIntentSuccess) + } + IatapayWebhookStatus::Initiated => Ok(Self::PaymentIntentProcessing), + IatapayWebhookStatus::Failed => Ok(Self::PaymentIntentFailure), + IatapayWebhookStatus::Created + | IatapayWebhookStatus::Cleared + | IatapayWebhookStatus::Tobeinvestigated + | IatapayWebhookStatus::Blocked + | IatapayWebhookStatus::UnexpectedSettled + | IatapayWebhookStatus::Unknown => Ok(Self::EventNotSupported), + }, + IatapayWebhookResponse::IatapayRefundWebhookBody(wh_body) => match wh_body.status { + IatapayRefundWebhookStatus::Authorized | IatapayRefundWebhookStatus::Settled => { + Ok(Self::RefundSuccess) + } + IatapayRefundWebhookStatus::Failed => Ok(Self::RefundFailure), + IatapayRefundWebhookStatus::Created + | IatapayRefundWebhookStatus::Locked + | IatapayRefundWebhookStatus::Initiated + | IatapayRefundWebhookStatus::Unknown => Ok(Self::EventNotSupported), + }, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum IatapayWebhookStatus { + Created, + Initiated, + Authorized, + Settled, + Cleared, + Failed, + Tobeinvestigated, + Blocked, + #[serde(rename = "UNEXPECTED SETTLED")] + UnexpectedSettled, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum IatapayRefundWebhookStatus { + Created, + Initiated, + Authorized, + Settled, + Failed, + Locked, + #[serde(other)] + Unknown, +} From 777771048a8144aac9e2f837c85531e139ecc125 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:37:35 +0530 Subject: [PATCH 382/443] feat(user): add support to delete user (#3374) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user_role.rs | 7 +- crates/api_models/src/user_role.rs | 7 ++ .../src/query/dashboard_metadata.rs | 14 +++ crates/diesel_models/src/query/user_role.rs | 15 +++- crates/router/src/core/errors/user.rs | 8 ++ crates/router/src/core/user_role.rs | 87 +++++++++++++++++++ crates/router/src/db/dashboard_metadata.rs | 48 ++++++++++ crates/router/src/db/kafka_store.rs | 21 ++++- crates/router/src/db/user_role.rs | 45 +++++++--- crates/router/src/routes/app.rs | 3 +- crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/user_role.rs | 18 ++++ .../authorization/predefined_permissions.rs | 16 ++++ crates/router_env/src/logger/types.rs | 2 + 14 files changed, 271 insertions(+), 21 deletions(-) diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index c8d8fd96a7a6..3ec30d6bd975 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -1,8 +1,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ - AcceptInvitationRequest, AuthorizationInfoResponse, GetRoleRequest, ListRolesResponse, - RoleInfoResponse, UpdateUserRoleRequest, + AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest, + ListRolesResponse, RoleInfoResponse, UpdateUserRoleRequest, }; common_utils::impl_misc_api_event_type!( @@ -11,5 +11,6 @@ common_utils::impl_misc_api_event_type!( GetRoleRequest, AuthorizationInfoResponse, UpdateUserRoleRequest, - AcceptInvitationRequest + AcceptInvitationRequest, + DeleteUserRoleRequest ); diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index d2548935f62a..e8c9b777c7f1 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -1,3 +1,5 @@ +use common_utils::pii; + use crate::user::DashboardEntryResponse; #[derive(Debug, serde::Serialize)] @@ -101,3 +103,8 @@ pub struct AcceptInvitationRequest { } pub type AcceptInvitationResponse = DashboardEntryResponse; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct DeleteUserRoleRequest { + pub email: pii::Email, +} diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index 678bcc2fd1f6..b1cb034eb1f6 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -104,4 +104,18 @@ impl DashboardMetadata { ) .await } + + pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + ) + .await + } } diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index 6b408038ef55..e67eba64c7cd 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -54,9 +54,18 @@ impl UserRole { .await } - pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult { - generics::generic_delete::<::Table, _>(conn, dsl::user_id.eq(user_id)) - .await + pub async fn delete_by_user_id_merchant_id( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)), + ) + .await } pub async fn list_by_user_id(conn: &PgPooledConn, user_id: String) -> StorageResult> { diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 330e02cd5471..f4000755b3ec 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -54,6 +54,8 @@ pub enum UserErrors { MerchantIdParsingError, #[error("ChangePasswordError")] ChangePasswordError, + #[error("InvalidDeleteOperation")] + InvalidDeleteOperation, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -157,6 +159,12 @@ impl common_utils::errors::ErrorSwitch AER::BadRequest(ApiError::new( + sub_code, + 30, + "Delete Operation Not Supported", + None, + )), } } } diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 245f8d246d23..742c281b89ad 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -1,6 +1,7 @@ use api_models::user_role as user_role_api; use diesel_models::{enums::UserStatus, user_role::UserRoleUpdate}; use error_stack::ResultExt; +use masking::ExposeInterface; use router_env::logger; use crate::{ @@ -11,6 +12,7 @@ use crate::{ authorization::{info, predefined_permissions}, ApplicationResponse, }, + types::domain, utils, }; @@ -161,3 +163,88 @@ pub async fn accept_invitation( Ok(ApplicationResponse::StatusOk) } + +pub async fn delete_user_role( + state: AppState, + user_from_token: auth::UserFromToken, + request: user_role_api::DeleteUserRoleRequest, +) -> UserResponse<()> { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email( + domain::UserEmail::from_pii_email(request.email)? + .get_secret() + .expose() + .as_str(), + ) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidRoleOperation) + .attach_printable("User not found in records") + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + if user_from_db.get_user_id() == user_from_token.user_id { + return Err(UserErrors::InvalidDeleteOperation.into()) + .attach_printable("User deleting himself"); + } + + let user_roles = state + .store + .list_user_roles_by_user_id(user_from_db.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)?; + + match user_roles + .iter() + .find(|&role| role.merchant_id == user_from_token.merchant_id.as_str()) + { + Some(user_role) => { + if !predefined_permissions::is_role_deletable(&user_role.role_id) { + return Err(UserErrors::InvalidRoleId.into()) + .attach_printable("Deletion not allowed for users with specific role id"); + } + } + None => { + return Err(UserErrors::InvalidDeleteOperation.into()) + .attach_printable("User is not associated with the merchant"); + } + }; + + if user_roles.len() > 1 { + state + .store + .delete_user_role_by_user_id_merchant_id( + user_from_db.get_user_id(), + user_from_token.merchant_id.as_str(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while deleting user role")?; + + Ok(ApplicationResponse::StatusOk) + } else { + state + .store + .delete_user_by_user_id(user_from_db.get_user_id()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while deleting user entry")?; + + state + .store + .delete_user_role_by_user_id_merchant_id( + user_from_db.get_user_id(), + user_from_token.merchant_id.as_str(), + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while deleting user role")?; + + Ok(ApplicationResponse::StatusOk) + } +} diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs index ec24b4ed07da..8e2ac0b6ad3f 100644 --- a/crates/router/src/db/dashboard_metadata.rs +++ b/crates/router/src/db/dashboard_metadata.rs @@ -36,6 +36,12 @@ pub trait DashboardMetadataInterface { org_id: &str, data_keys: Vec, ) -> CustomResult, errors::StorageError>; + + async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; } #[async_trait::async_trait] @@ -111,6 +117,21 @@ impl DashboardMetadataInterface for Store { .map_err(Into::into) .into_report() } + async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + ) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -246,4 +267,31 @@ impl DashboardMetadataInterface for MockDb { } Ok(query_result) } + async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + + let initial_len = dashboard_metadata.len(); + + dashboard_metadata.retain(|metadata_inner| { + !(metadata_inner + .user_id + .clone() + .map(|user_id_inner| user_id_inner == user_id) + .unwrap_or(false) + && metadata_inner.merchant_id == merchant_id) + }); + + if dashboard_metadata.len() == initial_len { + return Err(errors::StorageError::ValueNotFound(format!( + "No user available for user_id = {user_id} and merchant id = {merchant_id}" + )) + .into()); + } + + Ok(true) + } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 8398c153156d..e88d59ea9f39 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1955,9 +1955,14 @@ impl UserRoleInterface for KafkaStore { .update_user_role_by_user_id_merchant_id(user_id, merchant_id, update) .await } - - async fn delete_user_role(&self, user_id: &str) -> CustomResult { - self.diesel_store.delete_user_role(user_id).await + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_user_role_by_user_id_merchant_id(user_id, merchant_id) + .await } async fn list_user_roles_by_user_id( @@ -2017,6 +2022,16 @@ impl DashboardMetadataInterface for KafkaStore { .find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys) .await } + + async fn delete_user_scoped_dashboard_metadata_by_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index d8938f9683da..f02e6d60b3bc 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -32,8 +32,11 @@ pub trait UserRoleInterface { merchant_id: &str, update: storage::UserRoleUpdate, ) -> CustomResult; - - async fn delete_user_role(&self, user_id: &str) -> CustomResult; + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult; async fn list_user_roles_by_user_id( &self, @@ -100,12 +103,20 @@ impl UserRoleInterface for Store { .into_report() } - async fn delete_user_role(&self, user_id: &str) -> CustomResult { + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { let conn = connection::pg_connection_write(self).await?; - storage::UserRole::delete_by_user_id(&conn, user_id.to_owned()) - .await - .map_err(Into::into) - .into_report() + storage::UserRole::delete_by_user_id_merchant_id( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + ) + .await + .map_err(Into::into) + .into_report() } async fn list_user_roles_by_user_id( @@ -230,11 +241,17 @@ impl UserRoleInterface for MockDb { ) } - async fn delete_user_role(&self, user_id: &str) -> CustomResult { + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { let mut user_roles = self.user_roles.lock().await; let user_role_index = user_roles .iter() - .position(|user_role| user_role.user_id == user_id) + .position(|user_role| { + user_role.user_id == user_id && user_role.merchant_id == merchant_id + }) .ok_or(errors::StorageError::ValueNotFound(format!( "No user available for user_id = {user_id}" )))?; @@ -286,8 +303,14 @@ impl UserRoleInterface for super::KafkaStore { ) -> CustomResult { self.diesel_store.find_user_role_by_user_id(user_id).await } - async fn delete_user_role(&self, user_id: &str) -> CustomResult { - self.diesel_store.delete_user_role(user_id).await + async fn delete_user_role_by_user_id_merchant_id( + &self, + user_id: &str, + merchant_id: &str, + ) -> CustomResult { + self.diesel_store + .delete_user_role_by_user_id_merchant_id(user_id, merchant_id) + .await } async fn list_user_roles_by_user_id( &self, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index d3a43f0f490d..71c79295c73d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -974,7 +974,8 @@ impl User { web::resource("/data") .route(web::get().to(get_multiple_dashboard_metadata)) .route(web::post().to(set_dashboard_metadata)), - ); + ) + .service(web::resource("/user/delete").route(web::delete().to(delete_user_role))); #[cfg(feature = "dummy_connector")] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 1c967222dc7f..30348513c2b7 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -176,6 +176,7 @@ impl From for ApiIdentifier { | Flow::ForgotPassword | Flow::ResetPassword | Flow::InviteUser + | Flow::DeleteUser | Flow::UserSignUpWithMerchantId | Flow::VerifyEmail | Flow::VerifyEmailRequest diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 73b1ef1b01da..f83134e58251 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -115,3 +115,21 @@ pub async fn accept_invitation( )) .await } + +pub async fn delete_user_role( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::DeleteUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + user_role_core::delete_user_role, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authorization/predefined_permissions.rs b/crates/router/src/services/authorization/predefined_permissions.rs index c489f1fc9638..6fe0ddcc3605 100644 --- a/crates/router/src/services/authorization/predefined_permissions.rs +++ b/crates/router/src/services/authorization/predefined_permissions.rs @@ -9,6 +9,7 @@ pub struct RoleInfo { permissions: Vec, name: Option<&'static str>, is_invitable: bool, + is_deletable: bool, } impl RoleInfo { @@ -63,6 +64,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: None, is_invitable: false, + is_deletable: false, }, ); roles.insert( @@ -87,6 +89,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: None, is_invitable: false, + is_deletable: false, }, ); @@ -126,6 +129,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("Organization Admin"), is_invitable: false, + is_deletable: false, }, ); @@ -165,6 +169,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("Admin"), is_invitable: true, + is_deletable: true, }, ); roles.insert( @@ -189,6 +194,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("View Only"), is_invitable: true, + is_deletable: true, }, ); roles.insert( @@ -214,6 +220,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("IAM"), is_invitable: true, + is_deletable: true, }, ); roles.insert( @@ -239,6 +246,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("Developer"), is_invitable: true, + is_deletable: true, }, ); roles.insert( @@ -269,6 +277,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("Operator"), is_invitable: true, + is_deletable: true, }, ); roles.insert( @@ -291,6 +300,7 @@ pub static PREDEFINED_PERMISSIONS: Lazy> = Lazy: ], name: Some("Customer Support"), is_invitable: true, + is_deletable: true, }, ); roles @@ -307,3 +317,9 @@ pub fn is_role_invitable(role_id: &str) -> bool { .get(role_id) .map_or(false, |role_info| role_info.is_invitable) } + +pub fn is_role_deletable(role_id: &str) -> bool { + PREDEFINED_PERMISSIONS + .get(role_id) + .map_or(false, |role_info| role_info.is_deletable) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index ba323ebc5e3f..84f2e3e12674 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -321,6 +321,8 @@ pub enum Flow { ResetPassword, /// Invite users InviteUser, + /// Delete user + DeleteUser, /// Incremental Authorization flow PaymentsIncrementalAuthorization, /// Get action URL for connector onboarding From 3507ad60b2f1fd84d32eb4d97fe0a847db6f2045 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:38:50 +0530 Subject: [PATCH 383/443] fix(core): return surcharge in payment method list response if passed in create request (#3363) Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> --- .../src/connector/cybersource/transformers.rs | 6 +- .../router/src/core/payment_methods/cards.rs | 150 +++++---- .../surcharge_decision_configs.rs | 284 +++++++++++------- crates/router/src/core/payments.rs | 6 - 4 files changed, 249 insertions(+), 197 deletions(-) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 8beb81d92368..0abe1fff42c9 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -2015,9 +2015,9 @@ impl resource_id: types::ResponseId::NoResponseId, redirection_data, mandate_reference: None, - connector_metadata: Some( - serde_json::json!({"three_ds_data":three_ds_data}), - ), + connector_metadata: Some(serde_json::json!({ + "three_ds_data": three_ds_data + })), network_txn_id: None, connector_response_reference_id, incremental_authorization_allowed: None, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 712ae9e4035e..fbc4216fea43 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1864,49 +1864,45 @@ pub async fn call_surcharge_decision_management( billing_address: Option, response_payment_method_types: &mut [ResponsePaymentMethodsEnabled], ) -> errors::RouterResult { - if payment_attempt.surcharge_amount.is_some() { - Ok(api_surcharge_decision_configs::MerchantSurchargeConfigs::default()) - } else { - let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|val| val.parse_value("routing algorithm")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not decode the routing algorithm")? - .unwrap_or_default(); - let (surcharge_results, merchant_sucharge_configs) = - perform_surcharge_decision_management_for_payment_method_list( - &state, - algorithm_ref, - payment_attempt, - &payment_intent, - billing_address.as_ref().map(Into::into), - response_payment_method_types, + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let (surcharge_results, merchant_sucharge_configs) = + perform_surcharge_decision_management_for_payment_method_list( + &state, + algorithm_ref, + payment_attempt, + &payment_intent, + billing_address.as_ref().map(Into::into), + response_payment_method_types, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(&state, business_profile) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, ) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("error performing surcharge decision operation")?; - if !surcharge_results.is_empty_result() { - surcharge_results - .persist_individual_surcharge_details_in_redis(&state, business_profile) - .await?; - let _ = state - .store - .update_payment_intent( - payment_intent, - storage::PaymentIntentUpdate::SurchargeApplicableUpdate { - surcharge_applicable: true, - updated_by: merchant_account.storage_scheme.to_string(), - }, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) - .attach_printable("Failed to update surcharge_applicable in Payment Intent"); - } - Ok(merchant_sucharge_configs) + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); } + Ok(merchant_sucharge_configs) } pub async fn call_surcharge_decision_management_for_saved_card( @@ -1917,47 +1913,43 @@ pub async fn call_surcharge_decision_management_for_saved_card( payment_intent: storage::PaymentIntent, customer_payment_method_response: &mut api::CustomerPaymentMethodsListResponse, ) -> errors::RouterResult<()> { - if payment_attempt.surcharge_amount.is_some() { - Ok(()) - } else { - let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|val| val.parse_value("routing algorithm")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not decode the routing algorithm")? - .unwrap_or_default(); - let surcharge_results = perform_surcharge_decision_management_for_saved_cards( - state, - algorithm_ref, - payment_attempt, - &payment_intent, - &mut customer_payment_method_response.customer_payment_methods, - ) - .await + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("error performing surcharge decision operation")?; - if !surcharge_results.is_empty_result() { - surcharge_results - .persist_individual_surcharge_details_in_redis(state, business_profile) - .await?; - let _ = state - .store - .update_payment_intent( - payment_intent, - storage::PaymentIntentUpdate::SurchargeApplicableUpdate { - surcharge_applicable: true, - updated_by: merchant_account.storage_scheme.to_string(), - }, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) - .attach_printable("Failed to update surcharge_applicable in Payment Intent"); - } - Ok(()) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = perform_surcharge_decision_management_for_saved_cards( + state, + algorithm_ref, + payment_attempt, + &payment_intent, + &mut customer_payment_method_response.customer_payment_methods, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(state, business_profile) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); } + Ok(()) } #[allow(clippy::too_many_arguments)] diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index db1064b36a7a..f13c674eca72 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -1,7 +1,8 @@ +use std::sync::Arc; + use api_models::{ payment_methods::SurchargeDetailsResponse, - payments::Address, - routing, + payments, routing, surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord}, }; use common_utils::{ext_traits::StringExt, static_cache::StaticCache, types as common_utils_types}; @@ -15,7 +16,10 @@ use router_env::{instrument, tracing}; use crate::{ core::payments::{types, PaymentData}, db::StorageInterface, - types::{storage as oss_storage, transformers::ForeignTryFrom}, + types::{ + storage::{self as oss_storage, payment_attempt::PaymentAttemptExt}, + transformers::ForeignTryFrom, + }, }; static CONF_CACHE: StaticCache = StaticCache::new(); use crate::{ @@ -49,44 +53,102 @@ impl TryFrom for VirInterpreterBackendCacheWrapp } } +enum SurchargeSource { + /// Surcharge will be generated through the surcharge rules + Generate(Arc), + /// Surcharge is predefined by the merchant through payment create request + Predetermined(payments::RequestSurchargeDetails), +} + +impl SurchargeSource { + pub fn generate_surcharge_details_and_populate_surcharge_metadata( + &self, + backend_input: &backend::BackendInput, + payment_attempt: &oss_storage::PaymentAttempt, + surcharge_metadata_and_key: (&mut types::SurchargeMetadata, types::SurchargeKey), + ) -> ConditionalConfigResult> { + match self { + Self::Generate(interpreter) => { + let surcharge_output = execute_dsl_and_get_conditional_config( + backend_input.clone(), + &interpreter.cached_alogorith, + )?; + Ok(surcharge_output + .surcharge_details + .map(|surcharge_details| { + get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + ) + }) + .transpose()? + .map(|surcharge_details| { + let (surcharge_metadata, surcharge_key) = surcharge_metadata_and_key; + surcharge_metadata + .insert_surcharge_details(surcharge_key, surcharge_details.clone()); + surcharge_details + })) + } + Self::Predetermined(request_surcharge_details) => Ok(Some( + types::SurchargeDetails::from((request_surcharge_details, payment_attempt)), + )), + } + } +} + pub async fn perform_surcharge_decision_management_for_payment_method_list( state: &AppState, algorithm_ref: routing::RoutingAlgorithmRef, payment_attempt: &oss_storage::PaymentAttempt, payment_intent: &oss_storage::PaymentIntent, - billing_address: Option
, + billing_address: Option, response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], ) -> ConditionalConfigResult<( types::SurchargeMetadata, surcharge_decision_configs::MerchantSurchargeConfigs, )> { let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); - let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { - id - } else { - return Ok(( - surcharge_metadata, + + let (surcharge_source, merchant_surcharge_configs) = match ( + payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => ( + SurchargeSource::Predetermined(request_surcharge_details), surcharge_decision_configs::MerchantSurchargeConfigs::default(), - )); + ), + (None, Some(algorithm_id)) => { + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable( + "Unable to retrieve cached routing algorithm even after refresh", + )?; + let merchant_surcharge_config = cached_algo.merchant_surcharge_configs.clone(); + ( + SurchargeSource::Generate(cached_algo), + merchant_surcharge_config, + ) + } + (None, None) => { + return Ok(( + surcharge_metadata, + surcharge_decision_configs::MerchantSurchargeConfigs::default(), + )) + } }; - let key = ensure_algorithm_cached( - &*state.store, - &payment_attempt.merchant_id, - algorithm_ref.timestamp, - algorithm_id.as_str(), - ) - .await?; - let cached_algo = CONF_CACHE - .retrieve(&key) - .into_report() - .change_context(ConfigError::CacheMiss) - .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, billing_address) .change_context(ConfigError::InputConstructionError)?; - let interpreter = &cached_algo.cached_alogorith; - let merchant_surcharge_configs = cached_algo.merchant_surcharge_configs.clone(); for payment_methods_enabled in response_payment_method_types.iter_mut() { for payment_method_type_response in @@ -101,24 +163,21 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( for card_network_type in card_network_list.iter_mut() { backend_input.payment_method.card_network = Some(card_network_type.card_network.clone()); - let surcharge_output = - execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; - // let surcharge_details = - card_network_type.surcharge_details = surcharge_output - .surcharge_details - .map(|surcharge_details| { - let surcharge_details = get_surcharge_details_from_surcharge_output( - surcharge_details, - payment_attempt, - )?; - surcharge_metadata.insert_surcharge_details( + let surcharge_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + payment_attempt, + ( + &mut surcharge_metadata, types::SurchargeKey::PaymentMethodData( payment_methods_enabled.payment_method, payment_method_type_response.payment_method_type, Some(card_network_type.card_network.clone()), ), - surcharge_details.clone(), - ); + ), + )?; + card_network_type.surcharge_details = surcharge_details + .map(|surcharge_details| { SurchargeDetailsResponse::foreign_try_from(( &surcharge_details, payment_attempt, @@ -130,23 +189,21 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( .transpose()?; } } else { - let surcharge_output = - execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; - payment_method_type_response.surcharge_details = surcharge_output - .surcharge_details - .map(|surcharge_details| { - let surcharge_details = get_surcharge_details_from_surcharge_output( - surcharge_details, - payment_attempt, - )?; - surcharge_metadata.insert_surcharge_details( + let surcharge_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + payment_attempt, + ( + &mut surcharge_metadata, types::SurchargeKey::PaymentMethodData( payment_methods_enabled.payment_method, payment_method_type_response.payment_method_type, None, ), - surcharge_details.clone(), - ); + ), + )?; + payment_method_type_response.surcharge_details = surcharge_details + .map(|surcharge_details| { SurchargeDetailsResponse::foreign_try_from(( &surcharge_details, payment_attempt, @@ -173,51 +230,54 @@ where { let mut surcharge_metadata = types::SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); - let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { - id - } else { - return Ok(surcharge_metadata); + let surcharge_source = match ( + payment_data.payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => { + SurchargeSource::Predetermined(request_surcharge_details) + } + (None, Some(algorithm_id)) => { + let key = ensure_algorithm_cached( + &*state.store, + &payment_data.payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable( + "Unable to retrieve cached routing algorithm even after refresh", + )?; + SurchargeSource::Generate(cached_algo) + } + (None, None) => return Ok(surcharge_metadata), }; - - let key = ensure_algorithm_cached( - &*state.store, - &payment_data.payment_attempt.merchant_id, - algorithm_ref.timestamp, - algorithm_id.as_str(), - ) - .await?; - let cached_algo = CONF_CACHE - .retrieve(&key) - .into_report() - .change_context(ConfigError::CacheMiss) - .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; let mut backend_input = make_dsl_input_for_surcharge( &payment_data.payment_attempt, &payment_data.payment_intent, payment_data.address.billing.clone(), ) .change_context(ConfigError::InputConstructionError)?; - let interpreter = &cached_algo.cached_alogorith; for payment_method_type in payment_method_type_list { backend_input.payment_method.payment_method_type = Some(*payment_method_type); // in case of session flow, payment_method will always be wallet backend_input.payment_method.payment_method = Some(payment_method_type.to_owned().into()); - let surcharge_output = - execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; - if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details = get_surcharge_details_from_surcharge_output( - surcharge_details, - &payment_data.payment_attempt, - )?; - surcharge_metadata.insert_surcharge_details( + surcharge_source.generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, + &payment_data.payment_attempt, + ( + &mut surcharge_metadata, types::SurchargeKey::PaymentMethodData( payment_method_type.to_owned().into(), *payment_method_type, None, ), - surcharge_details, - ); - } + ), + )?; } Ok(surcharge_metadata) } @@ -229,27 +289,34 @@ pub async fn perform_surcharge_decision_management_for_saved_cards( customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], ) -> ConditionalConfigResult { let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); - let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { - id - } else { - return Ok(surcharge_metadata); + let surcharge_source = match ( + payment_attempt.get_surcharge_details(), + algorithm_ref.surcharge_config_algo_id, + ) { + (Some(request_surcharge_details), _) => { + SurchargeSource::Predetermined(request_surcharge_details) + } + (None, Some(algorithm_id)) => { + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable( + "Unable to retrieve cached routing algorithm even after refresh", + )?; + SurchargeSource::Generate(cached_algo) + } + (None, None) => return Ok(surcharge_metadata), }; - - let key = ensure_algorithm_cached( - &*state.store, - &payment_attempt.merchant_id, - algorithm_ref.timestamp, - algorithm_id.as_str(), - ) - .await?; - let cached_algo = CONF_CACHE - .retrieve(&key) - .into_report() - .change_context(ConfigError::CacheMiss) - .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) .change_context(ConfigError::InputConstructionError)?; - let interpreter = &cached_algo.cached_alogorith; for customer_payment_method in customer_payment_method_list.iter_mut() { backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); @@ -266,23 +333,22 @@ pub async fn perform_surcharge_decision_management_for_saved_cards( .change_context(ConfigError::DslExecutionError) }) .transpose()?; - let surcharge_output = - execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; - if let Some(surcharge_details_output) = surcharge_output.surcharge_details { - let surcharge_details = get_surcharge_details_from_surcharge_output( - surcharge_details_output, + let surcharge_details = surcharge_source + .generate_surcharge_details_and_populate_surcharge_metadata( + &backend_input, payment_attempt, + ( + &mut surcharge_metadata, + types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + ), )?; - surcharge_metadata.insert_surcharge_details( - types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), - surcharge_details.clone(), - ); - customer_payment_method.surcharge_details = Some( + customer_payment_method.surcharge_details = surcharge_details + .map(|surcharge_details| { SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) .into_report() - .change_context(ConfigError::DslParsingError)?, - ); - } + .change_context(ConfigError::DslParsingError) + }) + .transpose()?; } Ok(surcharge_metadata) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 043863a98fa3..21b82f307726 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -366,7 +366,6 @@ where call_surcharge_decision_management_for_session_flow( state, &merchant_account, - &business_profile, &mut payment_data, &connectors, ) @@ -599,7 +598,6 @@ pub fn get_connector_data( pub async fn call_surcharge_decision_management_for_session_flow( state: &AppState, merchant_account: &domain::MerchantAccount, - business_profile: &diesel_models::business_profile::BusinessProfile, payment_data: &mut PaymentData, session_connector_data: &[api::SessionConnectorData], ) -> RouterResult> @@ -644,10 +642,6 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; - surcharge_results - .persist_individual_surcharge_details_in_redis(state, business_profile) - .await?; - Ok(if surcharge_results.is_empty_result() { None } else { From fc6e68f7f07bf2d48466fa493596c0db02d7550a Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:41:54 +0530 Subject: [PATCH 384/443] feat(connector): [Adyen] Add support for PIX Payment Method (#3236) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 27 ++- .../stripe/payment_intents/types.rs | 5 +- .../stripe/setup_intents/types.rs | 5 +- .../src/connector/adyen/transformers.rs | 176 ++++++++++-------- crates/router/src/connector/utils.rs | 6 + .../router/src/core/payments/transformers.rs | 75 ++++++-- openapi/openapi_spec.json | 8 +- 7 files changed, 196 insertions(+), 106 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 06bd229586d9..24ac72175d71 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1892,8 +1892,12 @@ pub enum NextActionData { /// Contains url for Qr code image, this qr code has to be shown in sdk QrCodeInformation { #[schema(value_type = String)] - image_data_url: Url, + /// Hyperswitch generated image data source url + image_data_url: Option, display_to_timestamp: Option, + #[schema(value_type = String)] + /// The url for Qr code given by the connector + qr_code_url: Option, }, /// Contains the download url and the reference number for transaction DisplayVoucherInformation { @@ -1907,6 +1911,26 @@ pub enum NextActionData { }, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +// the enum order shouldn't be changed as this is being used during serialization and deserialization +pub enum QrCodeInformation { + QrCodeUrl { + image_data_url: Url, + qr_code_url: Url, + display_to_timestamp: Option, + }, + QrDataUrl { + image_data_url: Url, + display_to_timestamp: Option, + }, + QrCodeImageUrl { + qr_code_url: Url, + display_to_timestamp: Option, + }, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct BankTransferNextStepsData { /// The instructions for performing a bank transfer @@ -1932,6 +1956,7 @@ pub struct VoucherNextStepData { pub struct QrCodeNextStepsInstruction { pub image_data_url: Url, pub display_to_timestamp: Option, + pub qr_code_url: Option, } #[derive(Clone, Debug, serde::Deserialize)] diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 38007a3110d6..810e0ed1d284 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -794,8 +794,9 @@ pub enum StripeNextAction { session_token: Option, }, QrCodeInformation { - image_data_url: url::Url, + image_data_url: Option, display_to_timestamp: Option, + qr_code_url: Option, }, DisplayVoucherInformation { voucher_details: payments::VoucherNextStepData, @@ -830,9 +831,11 @@ pub(crate) fn into_stripe_next_action( payments::NextActionData::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, } => StripeNextAction::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, }, payments::NextActionData::DisplayVoucherInformation { voucher_details } => { StripeNextAction::DisplayVoucherInformation { voucher_details } diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs index 4c99d0cb00b4..5a2c7a02897e 100644 --- a/crates/router/src/compatibility/stripe/setup_intents/types.rs +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -384,8 +384,9 @@ pub enum StripeNextAction { session_token: Option, }, QrCodeInformation { - image_data_url: url::Url, + image_data_url: Option, display_to_timestamp: Option, + qr_code_url: Option, }, DisplayVoucherInformation { voucher_details: payments::VoucherNextStepData, @@ -420,9 +421,11 @@ pub(crate) fn into_stripe_next_action( payments::NextActionData::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, } => StripeNextAction::QrCodeInformation { image_data_url, display_to_timestamp, + qr_code_url, }, payments::NextActionData::DisplayVoucherInformation { voucher_details } => { StripeNextAction::DisplayVoucherInformation { voucher_details } diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 1e1cfa8fe50c..76678a1b33b0 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -343,6 +343,7 @@ pub struct QrCodeResponseResponse { action: AdyenQrCodeAction, refusal_reason: Option, refusal_reason_code: Option, + additional_data: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -352,10 +353,17 @@ pub struct AdyenQrCodeAction { #[serde(rename = "type")] type_of_response: ActionType, #[serde(rename = "url")] - mobile_redirection_url: Option, + qr_code_url: Option, qr_code_data: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QrCodeAdditionalData { + #[serde(rename = "pix.expirationDate")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pix_expiration_date: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenPtsAction { @@ -402,20 +410,20 @@ pub struct Amount { #[derive(Debug, Clone, Serialize)] #[serde(tag = "type")] pub enum AdyenPaymentMethod<'a> { - AdyenAffirm(Box), + AdyenAffirm(Box), AdyenCard(Box), - AdyenKlarna(Box), - AdyenPaypal(Box), + AdyenKlarna(Box), + AdyenPaypal(Box), #[serde(rename = "afterpaytouch")] - AfterPay(Box), - AlmaPayLater(Box), - AliPay(Box), - AliPayHk(Box), + AfterPay(Box), + AlmaPayLater(Box), + AliPay(Box), + AliPayHk(Box), ApplePay(Box), #[serde(rename = "atome")] Atome, BancontactCard(Box), - Bizum(Box), + Bizum(Box), Blik(Box), #[serde(rename = "boletobancario")] BoletoBancario, @@ -426,7 +434,7 @@ pub enum AdyenPaymentMethod<'a> { Eps(Box>), #[serde(rename = "gcash")] Gcash(Box), - Giropay(Box), + Giropay(Box), Gpay(Box), #[serde(rename = "gopay_wallet")] GoPay(Box), @@ -435,7 +443,7 @@ pub enum AdyenPaymentMethod<'a> { Kakaopay(Box), Mandate(Box), Mbway(Box), - MobilePay(Box), + MobilePay(Box), #[serde(rename = "momo_wallet")] Momo(Box), #[serde(rename = "momo_atm")] @@ -443,7 +451,7 @@ pub enum AdyenPaymentMethod<'a> { #[serde(rename = "touchngo")] TouchNGo(Box), OnlineBankingCzechRepublic(Box), - OnlineBankingFinland(Box), + OnlineBankingFinland(Box), OnlineBankingPoland(Box), OnlineBankingSlovakia(Box), #[serde(rename = "molpay_ebanking_fpx_MY")] @@ -513,6 +521,13 @@ pub enum AdyenPaymentMethod<'a> { Seicomart(Box), #[serde(rename = "econtext_stores")] PayEasy(Box), + Pix(Box), +} + +#[derive(Debug, Clone, Serialize)] +pub struct PmdForPaymentType { + #[serde(rename = "type")] + payment_type: PaymentType, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -592,11 +607,6 @@ pub struct BancontactCardData { holder_name: Secret, } -#[derive(Debug, Clone, Serialize)] -pub struct MobilePayData { - #[serde(rename = "type")] - payment_type: PaymentType, -} #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MbwayData { @@ -625,11 +635,6 @@ pub struct PayBrightData { payment_type: PaymentType, } -#[derive(Debug, Clone, Serialize)] -pub struct OnlineBankingFinlandData { - #[serde(rename = "type")] - payment_type: PaymentType, -} #[derive(Debug, Clone, Serialize)] pub struct OnlineBankingCzechRepublicData { #[serde(rename = "type")] @@ -1012,13 +1017,6 @@ pub struct BlikRedirectionData { blik_code: String, } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BankRedirectionPMData { - #[serde(rename = "type")] - payment_type: PaymentType, -} - #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BankRedirectionWithIssuer<'a> { @@ -1077,23 +1075,6 @@ pub enum CancelStatus { #[default] Processing, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AdyenPaypal { - #[serde(rename = "type")] - payment_type: PaymentType, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AliPayData { - #[serde(rename = "type")] - payment_type: PaymentType, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AliPayHkData { - #[serde(rename = "type")] - payment_type: PaymentType, -} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GoPayData {} @@ -1125,12 +1106,6 @@ pub struct AdyenApplePay { apple_pay_token: Secret, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AdyenPayLaterData { - #[serde(rename = "type")] - payment_type: PaymentType, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DokuBankData { @@ -1270,6 +1245,7 @@ pub enum PaymentType { Seicomart, #[serde(rename = "econtext_stores")] PayEasy, + Pix, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1948,19 +1924,19 @@ impl<'a> TryFrom<&api::WalletData> for AdyenPaymentMethod<'a> { Ok(AdyenPaymentMethod::ApplePay(Box::new(apple_pay_data))) } api_models::payments::WalletData::PaypalRedirect(_) => { - let wallet = AdyenPaypal { + let wallet = PmdForPaymentType { payment_type: PaymentType::Paypal, }; Ok(AdyenPaymentMethod::AdyenPaypal(Box::new(wallet))) } api_models::payments::WalletData::AliPayRedirect(_) => { - let alipay_data = AliPayData { + let alipay_data = PmdForPaymentType { payment_type: PaymentType::Alipay, }; Ok(AdyenPaymentMethod::AliPay(Box::new(alipay_data))) } api_models::payments::WalletData::AliPayHkRedirect(_) => { - let alipay_hk_data = AliPayHkData { + let alipay_hk_data = PmdForPaymentType { payment_type: PaymentType::AlipayHk, }; Ok(AdyenPaymentMethod::AliPayHk(Box::new(alipay_hk_data))) @@ -1993,7 +1969,7 @@ impl<'a> TryFrom<&api::WalletData> for AdyenPaymentMethod<'a> { Ok(AdyenPaymentMethod::Mbway(Box::new(mbway_data))) } api_models::payments::WalletData::MobilePayRedirect(_) => { - let data = MobilePayData { + let data = PmdForPaymentType { payment_type: PaymentType::MobilePay, }; Ok(AdyenPaymentMethod::MobilePay(Box::new(data))) @@ -2038,13 +2014,13 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> let (pay_later_data, country_code) = value; match pay_later_data { api_models::payments::PayLaterData::KlarnaRedirect { .. } => { - let klarna = AdyenPayLaterData { + let klarna = PmdForPaymentType { payment_type: PaymentType::Klarna, }; Ok(AdyenPaymentMethod::AdyenKlarna(Box::new(klarna))) } api_models::payments::PayLaterData::AffirmRedirect { .. } => Ok( - AdyenPaymentMethod::AdyenAffirm(Box::new(AdyenPayLaterData { + AdyenPaymentMethod::AdyenAffirm(Box::new(PmdForPaymentType { payment_type: PaymentType::Affirm, })), ), @@ -2055,7 +2031,7 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> | api_enums::CountryAlpha2::FR | api_enums::CountryAlpha2::ES | api_enums::CountryAlpha2::GB => Ok(AdyenPaymentMethod::ClearPay), - _ => Ok(AdyenPaymentMethod::AfterPay(Box::new(AdyenPayLaterData { + _ => Ok(AdyenPaymentMethod::AfterPay(Box::new(PmdForPaymentType { payment_type: PaymentType::Afterpaytouch, }))), } @@ -2072,7 +2048,7 @@ impl<'a> TryFrom<(&api::PayLaterData, Option)> Ok(AdyenPaymentMethod::Walley) } api_models::payments::PayLaterData::AlmaRedirect { .. } => Ok( - AdyenPaymentMethod::AlmaPayLater(Box::new(AdyenPayLaterData { + AdyenPaymentMethod::AlmaPayLater(Box::new(PmdForPaymentType { payment_type: PaymentType::Alma, })), ), @@ -2131,7 +2107,7 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod }, ))), api_models::payments::BankRedirectData::Bizum { .. } => { - Ok(AdyenPaymentMethod::Bizum(Box::new(BankRedirectionPMData { + Ok(AdyenPaymentMethod::Bizum(Box::new(PmdForPaymentType { payment_type: PaymentType::Bizum, }))) } @@ -2158,11 +2134,11 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod ), })), ), - api_models::payments::BankRedirectData::Giropay { .. } => Ok( - AdyenPaymentMethod::Giropay(Box::new(BankRedirectionPMData { + api_models::payments::BankRedirectData::Giropay { .. } => { + Ok(AdyenPaymentMethod::Giropay(Box::new(PmdForPaymentType { payment_type: PaymentType::Giropay, - })), - ), + }))) + } api_models::payments::BankRedirectData::Ideal { bank_name, .. } => Ok( AdyenPaymentMethod::Ideal(Box::new(BankRedirectionWithIssuer { payment_type: PaymentType::Ideal, @@ -2185,7 +2161,7 @@ impl<'a> TryFrom<&api_models::payments::BankRedirectData> for AdyenPaymentMethod ))) } api_models::payments::BankRedirectData::OnlineBankingFinland { .. } => Ok( - AdyenPaymentMethod::OnlineBankingFinland(Box::new(OnlineBankingFinlandData { + AdyenPaymentMethod::OnlineBankingFinland(Box::new(PmdForPaymentType { payment_type: PaymentType::OnlineBankingFinland, })), ), @@ -2296,8 +2272,12 @@ impl<'a> TryFrom<&api_models::payments::BankTransferData> for AdyenPaymentMethod last_name: billing_details.last_name.clone(), shopper_email: billing_details.email.clone(), }))), - api_models::payments::BankTransferData::Pix {} - | api_models::payments::BankTransferData::AchBankTransfer { .. } + api_models::payments::BankTransferData::Pix {} => { + Ok(AdyenPaymentMethod::Pix(Box::new(PmdForPaymentType { + payment_type: PaymentType::Pix, + }))) + } + api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::SepaBankTransfer { .. } | api_models::payments::BankTransferData::BacsBankTransfer { .. } | api_models::payments::BankTransferData::MultibancoBankTransfer { .. } @@ -3314,20 +3294,50 @@ pub fn get_qr_metadata( let image_data = crate_utils::QrImage::new_from_data(response.action.qr_code_data.to_owned()) .change_context(errors::ConnectorError::ResponseHandlingFailed)?; - let image_data_url = Url::parse(image_data.data.as_str()) - .ok() - .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; + let image_data_url = Url::parse(image_data.data.clone().as_str()).ok(); + let qr_code_url = response.action.qr_code_url.clone(); + let display_to_timestamp = response + .additional_data + .clone() + .and_then(|additional_data| additional_data.pix_expiration_date) + .map(|time| utils::get_timestamp_in_milliseconds(&time)); - let qr_code_instructions = payments::QrCodeNextStepsInstruction { - image_data_url, - display_to_timestamp: None, - }; + if let (Some(image_data_url), Some(qr_code_url)) = (image_data_url.clone(), qr_code_url.clone()) + { + let qr_code_info = payments::QrCodeInformation::QrCodeUrl { + image_data_url, + qr_code_url, + display_to_timestamp, + }; + Some(common_utils::ext_traits::Encode::< + payments::QrCodeInformation, + >::encode_to_value(&qr_code_info)) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } else if let (None, Some(qr_code_url)) = (image_data_url.clone(), qr_code_url.clone()) { + let qr_code_info = payments::QrCodeInformation::QrCodeImageUrl { + qr_code_url, + display_to_timestamp, + }; + Some(common_utils::ext_traits::Encode::< + payments::QrCodeInformation, + >::encode_to_value(&qr_code_info)) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } else if let (Some(image_data_url), None) = (image_data_url, qr_code_url) { + let qr_code_info = payments::QrCodeInformation::QrDataUrl { + image_data_url, + display_to_timestamp, + }; - Some(common_utils::ext_traits::Encode::< - payments::QrCodeNextStepsInstruction, - >::encode_to_value(&qr_code_instructions)) - .transpose() - .change_context(errors::ConnectorError::ResponseHandlingFailed) + Some(common_utils::ext_traits::Encode::< + payments::QrCodeInformation, + >::encode_to_value(&qr_code_info)) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } else { + Ok(None) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -3418,7 +3428,8 @@ pub fn get_wait_screen_metadata( | PaymentType::MiniStop | PaymentType::FamilyMart | PaymentType::Seicomart - | PaymentType::PayEasy => Ok(None), + | PaymentType::PayEasy + | PaymentType::Pix => Ok(None), } } @@ -3521,7 +3532,8 @@ pub fn get_present_to_shopper_metadata( | PaymentType::Vipps | PaymentType::Swish | PaymentType::PaySafeCard - | PaymentType::SevenEleven => Ok(None), + | PaymentType::SevenEleven + | PaymentType::Pix => Ok(None), } } diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 8f028e37a9e5..7226cb77342f 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -17,6 +17,7 @@ use masking::{ExposeInterface, Secret}; use once_cell::sync::Lazy; use regex::Regex; use serde::Serializer; +use time::PrimitiveDateTime; #[cfg(feature = "frm")] use crate::types::{fraud_check, storage::enums as storage_enums}; @@ -1607,6 +1608,11 @@ pub fn validate_currency( Ok(()) } +pub fn get_timestamp_in_milliseconds(datetime: &PrimitiveDateTime) -> i64 { + let utc_datetime = datetime.assume_utc(); + utc_datetime.unix_timestamp() * 1000 +} + #[cfg(feature = "frm")] pub trait FraudCheckSaleRequest { fn get_order_details(&self) -> Result, Error>; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index c7b1ecc26699..5f0c702a29d9 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -522,10 +522,7 @@ where } })) .or(next_action_containing_qr_code_url.map(|qr_code_data| { - api_models::payments::NextActionData::QrCodeInformation { - image_data_url: qr_code_data.image_data_url, - display_to_timestamp: qr_code_data.display_to_timestamp, - } + api_models::payments::NextActionData::foreign_from(qr_code_data) })) .or(next_action_containing_wait_screen.map(|wait_screen_data| { api_models::payments::NextActionData::WaitScreenInformation { @@ -830,11 +827,10 @@ where pub fn qr_code_next_steps_check( payment_attempt: storage::PaymentAttempt, -) -> RouterResult> { - let qr_code_steps: Option> = - payment_attempt - .connector_metadata - .map(|metadata| metadata.parse_value("QrCodeNextStepsInstruction")); +) -> RouterResult> { + let qr_code_steps: Option> = payment_attempt + .connector_metadata + .map(|metadata| metadata.parse_value("QrCodeInformation")); let qr_code_instructions = qr_code_steps.transpose().ok().flatten(); Ok(qr_code_instructions) @@ -904,17 +900,24 @@ pub fn bank_transfer_next_steps_check( let bank_transfer_next_step = if let Some(diesel_models::enums::PaymentMethod::BankTransfer) = payment_attempt.payment_method { - let bank_transfer_next_steps: Option = - payment_attempt - .connector_metadata - .map(|metadata| { - metadata - .parse_value("NextStepsRequirements") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse the Value to NextRequirements struct") - }) - .transpose()?; - bank_transfer_next_steps + if payment_attempt.payment_method_type != Some(diesel_models::enums::PaymentMethodType::Pix) + { + let bank_transfer_next_steps: Option = + payment_attempt + .connector_metadata + .map(|metadata| { + metadata + .parse_value("NextStepsRequirements") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to parse the Value to NextRequirements struct", + ) + }) + .transpose()?; + bank_transfer_next_steps + } else { + None + } } else { None }; @@ -960,6 +963,38 @@ pub fn change_order_details_to_new_type( }]) } +impl ForeignFrom for api_models::payments::NextActionData { + fn foreign_from(qr_info: api_models::payments::QrCodeInformation) -> Self { + match qr_info { + api_models::payments::QrCodeInformation::QrCodeUrl { + image_data_url, + qr_code_url, + display_to_timestamp, + } => Self::QrCodeInformation { + image_data_url: Some(image_data_url), + qr_code_url: Some(qr_code_url), + display_to_timestamp, + }, + api_models::payments::QrCodeInformation::QrDataUrl { + image_data_url, + display_to_timestamp, + } => Self::QrCodeInformation { + image_data_url: Some(image_data_url), + display_to_timestamp, + qr_code_url: None, + }, + api_models::payments::QrCodeInformation::QrCodeImageUrl { + qr_code_url, + display_to_timestamp, + } => Self::QrCodeInformation { + qr_code_url: Some(qr_code_url), + display_to_timestamp, + image_data_url: None, + }, + } + } +} + #[derive(Clone)] pub struct PaymentAdditionalData<'a, F> where diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 02df6324a06d..135ca638109e 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8221,17 +8221,23 @@ "description": "Contains url for Qr code image, this qr code has to be shown in sdk", "required": [ "image_data_url", + "qr_code_url", "type" ], "properties": { "image_data_url": { - "type": "string" + "type": "string", + "description": "Hyperswitch generated image data source url" }, "display_to_timestamp": { "type": "integer", "format": "int64", "nullable": true }, + "qr_code_url": { + "type": "string", + "description": "The url for Qr code given by the connector" + }, "type": { "type": "string", "enum": [ From 47fbe486cec252b8befca38f1b7ea77cc0823ee5 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:43:48 +0530 Subject: [PATCH 385/443] feat(core): [CYBERSOURCE] Add original authorized amount in router data (#3417) Co-authored-by: Samraat Bansal Co-authored-by: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> --- .../connector/bankofamerica/transformers.rs | 2 +- .../src/connector/cybersource/transformers.rs | 92 +++++++++++-------- crates/router/src/connector/utils.rs | 25 ++++- crates/router/src/core/payments.rs | 2 + crates/router/src/core/payments/helpers.rs | 25 +++++ 5 files changed, 106 insertions(+), 40 deletions(-) diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 72e3de0bf777..db96ff62f6ca 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -442,7 +442,7 @@ pub struct ClientRiskInformationRules { #[serde(rename_all = "camelCase")] pub struct Avs { code: String, - code_raw: String, + code_raw: Option, } impl diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 0abe1fff42c9..659f0733fdce 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -10,7 +10,7 @@ use crate::{ connector::utils::{ self, AddressDetailsData, ApplePayDecrypt, CardData, PaymentsAuthorizeRequestData, PaymentsCompleteAuthorizeRequestData, PaymentsPreProcessingData, - PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RouterData, + PaymentsSetupMandateRequestData, PaymentsSyncRequestData, RecurringMandateData, RouterData, }, consts, core::errors, @@ -47,6 +47,7 @@ impl T, ), ) -> Result { + // This conversion function is used at different places in the file, if updating this, keep a check for those let amount = utils::get_amount_as_string(currency_unit, amount, currency)?; Ok(Self { amount, @@ -81,11 +82,11 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { Some(vec![CybersourceActionsList::TokenCreate]), Some(vec![CybersourceActionsTokenType::PaymentInstrument]), Some(CybersourceAuthorizationOptions { - initiator: CybersourcePaymentInitiator { + initiator: Some(CybersourcePaymentInitiator { initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), credential_stored_on_file: Some(true), stored_credential_used: None, - }, + }), merchant_intitiated_transaction: None, }), ); @@ -272,14 +273,16 @@ pub enum CybersourceActionsTokenType { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceAuthorizationOptions { - initiator: CybersourcePaymentInitiator, + initiator: Option, merchant_intitiated_transaction: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct MerchantInitiatedTransaction { - reason: String, + reason: Option, + //Required for recurring mandates payment + original_authorized_amount: Option, } #[derive(Debug, Serialize)] @@ -470,35 +473,60 @@ impl From<&CybersourceRouterData<&types::PaymentsCompleteAuthorizeRouterData>> } impl - From<( + TryFrom<( &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, Option, )> for ProcessingInformation { - fn from( + type Error = error_stack::Report; + fn try_from( (item, solution): ( &CybersourceRouterData<&types::PaymentsAuthorizeRouterData>, Option, ), - ) -> Self { + ) -> Result { let (action_list, action_token_types, authorization_options) = if item.router_data.request.setup_mandate_details.is_some() { ( Some(vec![CybersourceActionsList::TokenCreate]), Some(vec![CybersourceActionsTokenType::PaymentInstrument]), Some(CybersourceAuthorizationOptions { - initiator: CybersourcePaymentInitiator { + initiator: Some(CybersourcePaymentInitiator { initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), credential_stored_on_file: Some(true), stored_credential_used: None, - }, + }), merchant_intitiated_transaction: None, }), ) + } else if item.router_data.request.connector_mandate_id().is_some() { + let original_amount = item + .router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_amount()?; + let original_currency = item + .router_data + .get_recurring_mandate_payment_data()? + .get_original_payment_currency()?; + ( + None, + None, + Some(CybersourceAuthorizationOptions { + initiator: None, + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: None, + original_authorized_amount: Some(utils::get_amount_as_string( + &types::api::CurrencyUnit::Base, + original_amount, + original_currency, + )?), + }), + }), + ) } else { (None, None, None) }; - Self { + Ok(Self { capture: Some(matches!( item.router_data.request.capture_method, Some(enums::CaptureMethod::Automatic) | None @@ -509,7 +537,7 @@ impl authorization_options, capture_options: None, commerce_indicator: String::from("internet"), - } + }) } } @@ -533,11 +561,11 @@ impl Some(vec![CybersourceActionsList::TokenCreate]), Some(vec![CybersourceActionsTokenType::PaymentInstrument]), Some(CybersourceAuthorizationOptions { - initiator: CybersourcePaymentInitiator { + initiator: Some(CybersourcePaymentInitiator { initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), credential_stored_on_file: Some(true), stored_credential_used: None, - }, + }), merchant_intitiated_transaction: None, }), ) @@ -680,7 +708,7 @@ impl }, }); - let processing_information = ProcessingInformation::from((item, None)); + let processing_information = ProcessingInformation::try_from((item, None))?; let client_reference_information = ClientReferenceInformation::from(item); let merchant_defined_information = item.router_data.request.metadata.clone().map(|metadata| { @@ -792,7 +820,7 @@ impl let bill_to = build_bill_to(item.router_data.get_billing()?, email)?; let order_information = OrderInformationWithBill::from((item, bill_to)); let processing_information = - ProcessingInformation::from((item, Some(PaymentSolution::ApplePay))); + ProcessingInformation::try_from((item, Some(PaymentSolution::ApplePay)))?; let client_reference_information = ClientReferenceInformation::from(item); let expiration_month = apple_pay_data.get_expiry_month()?; let expiration_year = apple_pay_data.get_four_digit_expiry_year()?; @@ -846,7 +874,7 @@ impl }, }); let processing_information = - ProcessingInformation::from((item, Some(PaymentSolution::GooglePay))); + ProcessingInformation::try_from((item, Some(PaymentSolution::GooglePay)))?; let client_reference_information = ClientReferenceInformation::from(item); let merchant_defined_information = item.router_data.request.metadata.clone().map(|metadata| { @@ -893,10 +921,9 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> build_bill_to(item.router_data.get_billing()?, email)?; let order_information = OrderInformationWithBill::from((item, bill_to)); - let processing_information = ProcessingInformation::from(( - item, - Some(PaymentSolution::ApplePay), - )); + let processing_information = ProcessingInformation::try_from( + (item, Some(PaymentSolution::ApplePay)), + )?; let client_reference_information = ClientReferenceInformation::from(item); let payment_information = PaymentInformation::ApplePayToken( @@ -1008,7 +1035,7 @@ impl String, ), ) -> Result { - let processing_information = ProcessingInformation::from((item, None)); + let processing_information = ProcessingInformation::try_from((item, None))?; let payment_instrument = CybersoucrePaymentInstrument { id: connector_mandate_id, }; @@ -1159,13 +1186,14 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout action_list: None, action_token_types: None, authorization_options: Some(CybersourceAuthorizationOptions { - initiator: CybersourcePaymentInitiator { + initiator: Some(CybersourcePaymentInitiator { initiator_type: None, credential_stored_on_file: None, stored_credential_used: Some(true), - }, + }), merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { - reason: "5".to_owned(), + reason: Some("5".to_owned()), + original_authorized_amount: None, }), }), commerce_indicator: String::from("internet"), @@ -1339,18 +1367,6 @@ impl From for common_enums::Authoriza } } -impl From for enums::RefundStatus { - fn from(item: CybersourcePaymentStatus) -> Self { - match item { - CybersourcePaymentStatus::Succeeded | CybersourcePaymentStatus::Transmitted => { - Self::Success - } - CybersourcePaymentStatus::Failed => Self::Failure, - _ => Self::Pending, - } - } -} - #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum CybersourcePaymentsResponse { @@ -1430,7 +1446,7 @@ pub struct ClientProcessorInformation { #[serde(rename_all = "camelCase")] pub struct Avs { code: String, - code_raw: String, + code_raw: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 7226cb77342f..3b3c992f0e00 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -25,7 +25,7 @@ use crate::{ consts, core::{ errors::{self, ApiErrorResponse, CustomResult}, - payments::PaymentData, + payments::{PaymentData, RecurringMandatePaymentData}, }, pii::PeekInterface, types::{ @@ -81,6 +81,7 @@ pub trait RouterData { fn get_customer_id(&self) -> Result; fn get_connector_customer_id(&self) -> Result; fn get_preprocessing_id(&self) -> Result; + fn get_recurring_mandate_payment_data(&self) -> Result; #[cfg(feature = "payouts")] fn get_payout_method_data(&self) -> Result; #[cfg(feature = "payouts")] @@ -250,6 +251,12 @@ impl RouterData for types::RouterData Result { + self.recurring_mandate_payment_data + .to_owned() + .ok_or_else(missing_field_err("recurring_mandate_payment_data")) + } + #[cfg(feature = "payouts")] fn get_payout_method_data(&self) -> Result { self.payout_method_data @@ -1133,6 +1140,22 @@ impl MandateData for payments::MandateAmountData { } } +pub trait RecurringMandateData { + fn get_original_payment_amount(&self) -> Result; + fn get_original_payment_currency(&self) -> Result; +} + +impl RecurringMandateData for RecurringMandatePaymentData { + fn get_original_payment_amount(&self) -> Result { + self.original_payment_authorized_amount + .ok_or_else(missing_field_err("original_payment_authorized_amount")) + } + fn get_original_payment_currency(&self) -> Result { + self.original_payment_authorized_currency + .ok_or_else(missing_field_err("original_payment_authorized_currency")) + } +} + pub trait MandateReferenceData { fn get_connector_mandate_id(&self) -> Result; } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 21b82f307726..186f760ace18 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -2039,6 +2039,8 @@ pub struct IncrementalAuthorizationDetails { #[derive(Debug, Default, Clone)] pub struct RecurringMandatePaymentData { pub payment_method_type: Option, //required for making recurring payment using saved payment method through stripe + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 213adc79fb01..0cbed255348e 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -479,6 +479,27 @@ pub async fn get_token_for_recurring_mandate( .await .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + let original_payment_intent = mandate + .original_payment_id + .as_ref() + .async_map(|payment_id| async { + db.find_payment_intent_by_payment_id_merchant_id( + payment_id, + &mandate.merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .map_err(|err| logger::error!(mandate_original_payment_not_found=?err)) + .ok() + }) + .await + .flatten(); + + let original_payment_authorized_amount = original_payment_intent.clone().map(|pi| pi.amount); + let original_payment_authorized_currency = + original_payment_intent.clone().and_then(|pi| pi.currency); + let customer = req.customer_id.clone().get_required_value("customer_id")?; let payment_method_id = { @@ -540,6 +561,8 @@ pub async fn get_token_for_recurring_mandate( Some(payment_method.payment_method), Some(payments::RecurringMandatePaymentData { payment_method_type, + original_payment_authorized_amount, + original_payment_authorized_currency, }), payment_method.payment_method_type, Some(mandate_connector_details), @@ -550,6 +573,8 @@ pub async fn get_token_for_recurring_mandate( Some(payment_method.payment_method), Some(payments::RecurringMandatePaymentData { payment_method_type, + original_payment_authorized_amount, + original_payment_authorized_currency, }), payment_method.payment_method_type, Some(mandate_connector_details), From c2946cfe05ffa81a66643e04eff5e89b545d2d43 Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Thu, 25 Jan 2024 12:56:20 +0530 Subject: [PATCH 386/443] feat(logging): add a logging middleware to log all api requests (#3437) --- crates/drainer/src/query.rs | 2 +- .../router/src/core/payment_methods/cards.rs | 8 +- .../router/src/core/payments/tokenization.rs | 4 +- crates/router/src/lib.rs | 1 + crates/router/src/middleware.rs | 94 ++++++++++++++++--- crates/router/src/routes/admin.rs | 4 +- crates/router/src/routes/mandates.rs | 4 +- crates/router/src/routes/payment_methods.rs | 8 +- crates/router/src/services/api.rs | 1 - crates/router_env/src/logger/storage.rs | 16 +++- docker-compose-development.yml | 2 +- docker-compose.yml | 2 +- 12 files changed, 120 insertions(+), 26 deletions(-) diff --git a/crates/drainer/src/query.rs b/crates/drainer/src/query.rs index f79291f3eae6..a1e04fb6d0f1 100644 --- a/crates/drainer/src/query.rs +++ b/crates/drainer/src/query.rs @@ -37,7 +37,7 @@ impl ExecuteQuery for kv::DBOperation { ]; let (result, execution_time) = - common_utils::date_time::time_it(|| self.execute(&conn)).await; + Box::pin(common_utils::date_time::time_it(|| self.execute(&conn))).await; push_drainer_delay(pushed_at, operation, table, tags); metrics::QUERY_EXECUTION_TIME.record(&metrics::CONTEXT, execution_time, tags); diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index fbc4216fea43..3dbd6fcb215b 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -245,7 +245,13 @@ pub async fn update_customer_payment_method( .as_ref() .map(|card_network| card_network.to_string()), }; - add_payment_method(state, new_pm, &merchant_account, &key_store).await + Box::pin(add_payment_method( + state, + new_pm, + &merchant_account, + &key_store, + )) + .await } // Wrapper function to switch lockers diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 15d88c94660c..f52bce46d8ee 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -81,11 +81,11 @@ where ) .await? } else { - save_in_locker( + Box::pin(save_in_locker( state, merchant_account, payment_method_create_request.to_owned(), - ) + )) .await? }; diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index c38a4dc85b55..7b1aba14106f 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -267,5 +267,6 @@ pub fn get_application_builder( .wrap(middleware::default_response_headers()) .wrap(middleware::RequestId) .wrap(cors::cors()) + .wrap(middleware::LogSpanInitializer) .wrap(router_env::tracing_actix_web::TracingLogger::default()) } diff --git a/crates/router/src/middleware.rs b/crates/router/src/middleware.rs index c6c94d3a78ea..1feba66a34f8 100644 --- a/crates/router/src/middleware.rs +++ b/crates/router/src/middleware.rs @@ -1,3 +1,4 @@ +use router_env::tracing::{field::Empty, Instrument}; /// Middleware to include request ID in response header. pub struct RequestId; @@ -48,20 +49,23 @@ where let request_id_fut = req.extract::(); let response_fut = self.service.call(req); - Box::pin(async move { - let request_id = request_id_fut.await?; - let request_id = request_id.as_hyphenated().to_string(); - if let Some(upstream_request_id) = old_x_request_id { - router_env::logger::info!(?request_id, ?upstream_request_id); + Box::pin( + async move { + let request_id = request_id_fut.await?; + let request_id = request_id.as_hyphenated().to_string(); + if let Some(upstream_request_id) = old_x_request_id { + router_env::logger::info!(?request_id, ?upstream_request_id); + } + let mut response = response_fut.await?; + response.headers_mut().append( + http::header::HeaderName::from_static("x-request-id"), + http::HeaderValue::from_str(&request_id)?, + ); + + Ok(response) } - let mut response = response_fut.await?; - response.headers_mut().append( - http::header::HeaderName::from_static("x-request-id"), - http::HeaderValue::from_str(&request_id)?, - ); - - Ok(response) - }) + .in_current_span(), + ) } } @@ -81,3 +85,67 @@ pub fn default_response_headers() -> actix_web::middleware::DefaultHeaders { .add((header::STRICT_TRANSPORT_SECURITY, "max-age=31536000")) .add((header::VIA, "HyperSwitch")) } + +/// Middleware to build a TOP level domain span for each request. +pub struct LogSpanInitializer; + +impl actix_web::dev::Transform for LogSpanInitializer +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Transform = LogSpanInitializerMiddleware; + type InitError = (); + type Future = std::future::Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + std::future::ready(Ok(LogSpanInitializerMiddleware { service })) + } +} + +pub struct LogSpanInitializerMiddleware { + service: S, +} + +impl actix_web::dev::Service + for LogSpanInitializerMiddleware +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Future = futures::future::LocalBoxFuture<'static, Result>; + + actix_web::dev::forward_ready!(service); + + // TODO: have a common source of truth for the list of top level fields + // /crates/router_env/src/logger/storage.rs also has a list of fields called PERSISTENT_KEYS + fn call(&self, req: actix_web::dev::ServiceRequest) -> Self::Future { + let response_fut = self.service.call(req); + + Box::pin( + response_fut.instrument( + router_env::tracing::info_span!( + "golden_log_line", + payment_id = Empty, + merchant_id = Empty, + connector_name = Empty + ) + .or_current(), + ), + ) + } +} diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index ab404125a384..f5a6a49b9c95 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -520,7 +520,7 @@ pub async fn business_profile_update( let flow = Flow::BusinessProfileUpdate; let (merchant_id, profile_id) = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -535,7 +535,7 @@ pub async fn business_profile_update( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::BusinessProfileDelete))] diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index 468d202b94c5..342fe23c38ae 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -75,7 +75,7 @@ pub async fn revoke_mandate( let mandate_id = mandates::MandateId { mandate_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -85,7 +85,7 @@ pub async fn revoke_mandate( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Mandates - List Mandates diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 6ef5de886be4..55564a6386f4 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -44,7 +44,13 @@ pub async fn create_payment_method_api( &req, json_payload.into_inner(), |state, auth, req| async move { - cards::add_payment_method(state, req, &auth.merchant_account, &auth.key_store).await + Box::pin(cards::add_payment_method( + state, + req, + &auth.merchant_account, + &auth.key_store, + )) + .await }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index ad463fcf2b92..307dff550717 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1131,7 +1131,6 @@ where status_code = response_code, time_taken_ms = request_duration.as_millis(), ); - res } diff --git a/crates/router_env/src/logger/storage.rs b/crates/router_env/src/logger/storage.rs index 51e701213b9c..961a77c65aa7 100644 --- a/crates/router_env/src/logger/storage.rs +++ b/crates/router_env/src/logger/storage.rs @@ -92,6 +92,8 @@ impl Visit for Storage<'_> { } } +const PERSISTENT_KEYS: [&str; 3] = ["payment_id", "connector_name", "merchant_id"]; + impl tracing_subscriber::registry::LookupSpan<'a>> Layer for StorageSubscription { @@ -99,6 +101,7 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { #[allow(clippy::expect_used)] let span = ctx.span(id).expect("No span"); + let mut extensions = span.extensions_mut(); let mut visitor = if let Some(parent_span) = span.parent() { let mut extensions = parent_span.extensions_mut(); @@ -110,7 +113,6 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer Storage::default() }; - let mut extensions = span.extensions_mut(); attrs.record(&mut visitor); extensions.insert(visitor); } @@ -150,6 +152,18 @@ impl tracing_subscriber::registry::LookupSpan<'a>> Layer .unwrap_or(0) }; + if let Some(s) = span.extensions().get::>() { + s.values.iter().for_each(|(k, v)| { + if PERSISTENT_KEYS.contains(k) { + span.parent().and_then(|p| { + p.extensions_mut() + .get_mut::>() + .map(|s| s.values.insert(k, v.to_owned())) + }); + } + }) + }; + let mut extensions_mut = span.extensions_mut(); #[allow(clippy::expect_used)] let visitor = extensions_mut diff --git a/docker-compose-development.yml b/docker-compose-development.yml index 5a3eca4cdf35..665bdf2f05db 100644 --- a/docker-compose-development.yml +++ b/docker-compose-development.yml @@ -31,7 +31,7 @@ services: networks: - router_net ports: - - "6379" + - "6379:6379" migration_runner: image: rust:latest diff --git a/docker-compose.yml b/docker-compose.yml index 9f8e7bb4efba..3839269a5220 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,7 @@ services: networks: - router_net ports: - - "6379" + - "6379:6379" migration_runner: image: rust:latest From d5e9866b522bad3e62f6f6c0d7993f5dcc2939af Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:11:10 +0530 Subject: [PATCH 387/443] feat(core): Add outgoing webhook for manual `partial_capture` events (#3388) --- crates/router/src/utils.rs | 76 ++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index aaa145099e4e..68909d1127c5 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -51,7 +51,7 @@ use crate::{ types::{encrypt_optional, AsyncLift}, }, storage, - transformers::{ForeignTryFrom, ForeignTryInto}, + transformers::ForeignFrom, }, }; @@ -681,23 +681,6 @@ pub fn add_apple_pay_payment_status_metrics( } } -impl ForeignTryFrom for enums::EventType { - type Error = errors::ValidationError; - - fn foreign_try_from(value: enums::IntentStatus) -> Result { - match value { - enums::IntentStatus::Succeeded => Ok(Self::PaymentSucceeded), - enums::IntentStatus::Failed => Ok(Self::PaymentFailed), - enums::IntentStatus::Processing => Ok(Self::PaymentProcessing), - enums::IntentStatus::RequiresMerchantAction - | enums::IntentStatus::RequiresCustomerAction => Ok(Self::ActionRequired), - _ => Err(errors::ValidationError::IncorrectValueProvided { - field_name: "intent_status", - }), - } - } -} - pub async fn trigger_payments_webhook( merchant_account: domain::MerchantAccount, business_profile: diesel_models::business_profile::BusinessProfile, @@ -726,7 +709,9 @@ where if matches!( status, - enums::IntentStatus::Succeeded | enums::IntentStatus::Failed + enums::IntentStatus::Succeeded + | enums::IntentStatus::Failed + | enums::IntentStatus::PartiallyCaptured ) { let payments_response = crate::core::payments::transformers::payments_to_payments_response( req, @@ -742,11 +727,7 @@ where None, )?; - let event_type: enums::EventType = status - .foreign_try_into() - .into_report() - .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) - .attach_printable("payment event type mapping failed")?; + let event_type = ForeignFrom::foreign_from(status); if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) = payments_response @@ -755,27 +736,34 @@ where // This spawns this futures in a background thread, the exception inside this future won't affect // the current thread and the lifecycle of spawn thread is not handled by runtime. // So when server shutdown won't wait for this thread's completion. - tokio::spawn( - async move { - Box::pin( - webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( - m_state, - merchant_account, - business_profile, - event_type, - diesel_models::enums::EventClass::Payments, - None, - payment_id, - diesel_models::enums::EventObjectType::PaymentDetails, - webhooks::OutgoingWebhookContent::PaymentDetails( - payments_response_json, + + if let Some(event_type) = event_type { + tokio::spawn( + async move { + Box::pin( + webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( + m_state, + merchant_account, + business_profile, + event_type, + diesel_models::enums::EventClass::Payments, + None, + payment_id, + diesel_models::enums::EventObjectType::PaymentDetails, + webhooks::OutgoingWebhookContent::PaymentDetails( + payments_response_json, + ), ), - ), - ) - .await - } - .in_current_span(), - ); + ) + .await + } + .in_current_span(), + ); + } else { + logger::warn!( + "Outgoing webhook not sent because of missing event type status mapping" + ); + } } } From d827c9af29b8516f379e648e00f4ab307ae1a34d Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 25 Jan 2024 10:48:02 +0100 Subject: [PATCH 388/443] fix(connector): Use `ConnectorError::InvalidConnectorConfig` for an invalid `CoinbaseConnectorMeta` (#3168) Co-authored-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- .../src/connector/coinbase/transformers.rs | 19 ++++++++++++++----- crates/router/src/connector/utils.rs | 17 ----------------- crates/router/src/core/admin.rs | 2 +- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/crates/router/src/connector/coinbase/transformers.rs b/crates/router/src/connector/coinbase/transformers.rs index ce9bb3e871c5..b1435e50df9d 100644 --- a/crates/router/src/connector/coinbase/transformers.rs +++ b/crates/router/src/connector/coinbase/transformers.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use common_utils::pii; +use error_stack::ResultExt; use serde::{Deserialize, Serialize}; use crate::{ @@ -250,6 +252,14 @@ pub struct CoinbaseConnectorMeta { pub pricing_type: String, } +impl TryFrom<&Option> for CoinbaseConnectorMeta { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + utils::to_connector_meta_from_secret(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { config: "metadata" }) + } +} + fn get_crypto_specific_payment_data( item: &types::PaymentsAuthorizeRouterData, ) -> Result> { @@ -260,11 +270,10 @@ fn get_crypto_specific_payment_data( let name = billing_address.and_then(|add| add.get_first_name().ok().map(|name| name.to_owned())); let description = item.get_description().ok(); - let connector_meta: CoinbaseConnectorMeta = - utils::to_connector_meta_from_secret_with_required_field( - item.connector_meta_data.clone(), - "Pricing Type Not present in connector meta data", - )?; + let connector_meta = CoinbaseConnectorMeta::try_from(&item.connector_meta_data) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "Merchant connector account metadata", + })?; let pricing_type = connector_meta.pricing_type; let local_price = get_local_price(item); let redirect_url = item.request.get_return_url()?; diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 3b3c992f0e00..87d50bb68a2c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1222,23 +1222,6 @@ where json.parse_value(std::any::type_name::()).switch() } -pub fn to_connector_meta_from_secret_with_required_field( - connector_meta: Option>, - error_message: &'static str, -) -> Result -where - T: serde::de::DeserializeOwned, -{ - let connector_error = errors::ConnectorError::MissingRequiredField { - field_name: error_message, - }; - let parsed_meta = to_connector_meta_from_secret(connector_meta).ok(); - match parsed_meta { - Some(meta) => Ok(meta), - _ => Err(connector_error.into()), - } -} - pub fn to_connector_meta_from_secret( connector_meta: Option>, ) -> Result diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index fd4cae3a2b9b..364c5b9b2124 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1713,9 +1713,9 @@ pub(crate) fn validate_auth_and_metadata_type( checkout::transformers::CheckoutAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Coinbase => { coinbase::transformers::CoinbaseAuthType::try_from(val)?; + coinbase::transformers::CoinbaseConnectorMeta::try_from(connector_meta_data)?; Ok(()) } api_enums::Connector::Cryptopay => { From 9a54838b0529013ab8f449ec6b347a104b55f8f7 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:45:49 +0530 Subject: [PATCH 389/443] fix(connector): fix connector template script (#3453) --- connector-template/mod.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/connector-template/mod.rs b/connector-template/mod.rs index 6258d4370768..c64ce431968c 100644 --- a/connector-template/mod.rs +++ b/connector-template/mod.rs @@ -167,10 +167,8 @@ impl req.request.amount, req, ))?; - let req_obj = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest::try_from(&connector_router_data)?; - let {{project-name | downcase}}_req = types::RequestBody::log_and_get_request_body(&req_obj, utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest>::encode_to_string_of_json) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some({{project-name | downcase}}_req)) + let connector_req = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}PaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( @@ -385,10 +383,8 @@ impl req.request.refund_amount, req, ))?; - let req_obj = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundRequest::try_from(&connector_router_data)?; - let {{project-name | downcase}}_req = types::RequestBody::log_and_get_request_body(&req_obj, utils::Encode::<{{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundRequest>::encode_to_string_of_json) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(Some({{project-name | downcase}}_req)) + let connector_req = {{project-name | downcase}}::{{project-name | downcase | pascal_case}}RefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request(&self, req: &types::RefundsRouterData, connectors: &settings::Connectors,) -> CustomResult,errors::ConnectorError> { From ec859eabbfb8a511f0fffd30a47a144fb07f2886 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:16:03 +0530 Subject: [PATCH 390/443] fix(connector): [HELCIM] Handle 4XX Errors (#3458) --- crates/router/src/connector/helcim.rs | 9 ++++++--- crates/router/src/connector/helcim/transformers.rs | 9 ++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/helcim.rs b/crates/router/src/connector/helcim.rs index c3a6c5e9f503..0b2cac5d7660 100644 --- a/crates/router/src/connector/helcim.rs +++ b/crates/router/src/connector/helcim.rs @@ -128,9 +128,12 @@ impl ConnectorCommon for Helcim { .response .parse_struct("HelcimErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let error_string = match response.errors { - transformers::HelcimErrorTypes::StringType(error) => error, - transformers::HelcimErrorTypes::JsonType(error) => error.to_string(), + let error_string = match response { + transformers::HelcimErrorResponse::Payment(response) => match response.errors { + transformers::HelcimErrorTypes::StringType(error) => error, + transformers::HelcimErrorTypes::JsonType(error) => error.to_string(), + }, + transformers::HelcimErrorResponse::General(error_string) => error_string, }; Ok(ErrorResponse { diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 599054163c3d..5e076be651f5 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -756,6 +756,13 @@ pub enum HelcimErrorTypes { } #[derive(Debug, Deserialize)] -pub struct HelcimErrorResponse { +pub struct HelcimPaymentsErrorResponse { pub errors: HelcimErrorTypes, } + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum HelcimErrorResponse { + Payment(HelcimPaymentsErrorResponse), + General(String), +} From a59ac7d5b98f27f5fb34206c20ef9c37a07259a3 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:54:13 +0530 Subject: [PATCH 391/443] feat(user): support multiple invites (#3422) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/user.rs | 10 ++ crates/router/src/core/errors/user.rs | 145 ++++++++++--------- crates/router/src/core/user.rs | 187 ++++++++++++++++++++++++- crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/user.rs | 17 +++ crates/router_env/src/logger/types.rs | 2 + 7 files changed, 296 insertions(+), 69 deletions(-) diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 8de6a3c0b4fa..056d1b593dc9 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -89,6 +89,16 @@ pub struct InviteUserResponse { pub password: Option>, } +#[derive(Debug, serde::Serialize)] +pub struct InviteMultipleUserResponse { + pub email: pii::Email, + pub is_email_sent: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct SwitchMerchantIdRequest { pub merchant_id: String, diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index f4000755b3ec..389cb10d7b53 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -56,6 +56,8 @@ pub enum UserErrors { ChangePasswordError, #[error("InvalidDeleteOperation")] InvalidDeleteOperation, + #[error("MaxInvitationsError")] + MaxInvitationsError, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -64,107 +66,118 @@ impl common_utils::errors::ErrorSwitch { - AER::InternalServerError(ApiError::new("HE", 0, "Something Went Wrong", None)) + AER::InternalServerError(ApiError::new("HE", 0, self.get_error_message(), None)) + } + Self::InvalidCredentials => { + AER::Unauthorized(ApiError::new(sub_code, 1, self.get_error_message(), None)) + } + Self::UserNotFound => { + AER::Unauthorized(ApiError::new(sub_code, 2, self.get_error_message(), None)) + } + Self::UserExists => { + AER::BadRequest(ApiError::new(sub_code, 3, self.get_error_message(), None)) } - Self::InvalidCredentials => AER::Unauthorized(ApiError::new( - sub_code, - 1, - "Incorrect email or password", - None, - )), - Self::UserNotFound => AER::Unauthorized(ApiError::new( - sub_code, - 2, - "Email doesn’t exist. Register", - None, - )), - Self::UserExists => AER::BadRequest(ApiError::new( - sub_code, - 3, - "An account already exists with this email", - None, - )), Self::LinkInvalid => { - AER::Unauthorized(ApiError::new(sub_code, 4, "Invalid or expired link", None)) + AER::Unauthorized(ApiError::new(sub_code, 4, self.get_error_message(), None)) + } + Self::UnverifiedUser => { + AER::Unauthorized(ApiError::new(sub_code, 5, self.get_error_message(), None)) + } + Self::InvalidOldPassword => { + AER::BadRequest(ApiError::new(sub_code, 6, self.get_error_message(), None)) } - Self::UnverifiedUser => AER::Unauthorized(ApiError::new( - sub_code, - 5, - "Kindly verify your account", - None, - )), - Self::InvalidOldPassword => AER::BadRequest(ApiError::new( - sub_code, - 6, - "Old password incorrect. Please enter the correct password", - None, - )), Self::EmailParsingError => { - AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) + AER::BadRequest(ApiError::new(sub_code, 7, self.get_error_message(), None)) } Self::NameParsingError => { - AER::BadRequest(ApiError::new(sub_code, 8, "Invalid Name", None)) + AER::BadRequest(ApiError::new(sub_code, 8, self.get_error_message(), None)) } Self::PasswordParsingError => { - AER::BadRequest(ApiError::new(sub_code, 9, "Invalid Password", None)) + AER::BadRequest(ApiError::new(sub_code, 9, self.get_error_message(), None)) } Self::UserAlreadyVerified => { - AER::Unauthorized(ApiError::new(sub_code, 11, "User already verified", None)) + AER::Unauthorized(ApiError::new(sub_code, 11, self.get_error_message(), None)) } Self::CompanyNameParsingError => { - AER::BadRequest(ApiError::new(sub_code, 14, "Invalid Company Name", None)) + AER::BadRequest(ApiError::new(sub_code, 14, self.get_error_message(), None)) } Self::MerchantAccountCreationError(error_message) => { AER::InternalServerError(ApiError::new(sub_code, 15, error_message, None)) } Self::InvalidEmailError => { - AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) + AER::BadRequest(ApiError::new(sub_code, 16, self.get_error_message(), None)) } Self::MerchantIdNotFound => { - AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) + AER::BadRequest(ApiError::new(sub_code, 18, self.get_error_message(), None)) } Self::MetadataAlreadySet => { - AER::BadRequest(ApiError::new(sub_code, 19, "Metadata already set", None)) + AER::BadRequest(ApiError::new(sub_code, 19, self.get_error_message(), None)) } Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( sub_code, 21, - "An Organization with the id already exists", + self.get_error_message(), None, )), Self::InvalidRoleId => { - AER::BadRequest(ApiError::new(sub_code, 22, "Invalid Role ID", None)) + AER::BadRequest(ApiError::new(sub_code, 22, self.get_error_message(), None)) } - Self::InvalidRoleOperation => AER::BadRequest(ApiError::new( - sub_code, - 23, - "User Role Operation Not Supported", - None, - )), - Self::IpAddressParsingFailed => { - AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) + Self::InvalidRoleOperation => { + AER::BadRequest(ApiError::new(sub_code, 23, self.get_error_message(), None)) } - Self::InvalidMetadataRequest => AER::BadRequest(ApiError::new( + Self::IpAddressParsingFailed => AER::InternalServerError(ApiError::new( sub_code, - 26, - "Invalid Metadata Request", + 24, + self.get_error_message(), None, )), + Self::InvalidMetadataRequest => { + AER::BadRequest(ApiError::new(sub_code, 26, self.get_error_message(), None)) + } Self::MerchantIdParsingError => { - AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None)) + AER::BadRequest(ApiError::new(sub_code, 28, self.get_error_message(), None)) } - Self::ChangePasswordError => AER::BadRequest(ApiError::new( - sub_code, - 29, - "Old and new password cannot be same", - None, - )), - Self::InvalidDeleteOperation => AER::BadRequest(ApiError::new( - sub_code, - 30, - "Delete Operation Not Supported", - None, - )), + Self::ChangePasswordError => { + AER::BadRequest(ApiError::new(sub_code, 29, self.get_error_message(), None)) + } + Self::InvalidDeleteOperation => { + AER::BadRequest(ApiError::new(sub_code, 30, self.get_error_message(), None)) + } + Self::MaxInvitationsError => { + AER::BadRequest(ApiError::new(sub_code, 31, self.get_error_message(), None)) + } + } + } +} + +impl UserErrors { + pub fn get_error_message(&self) -> &str { + match self { + Self::InternalServerError => "Something went wrong", + Self::InvalidCredentials => "Incorrect email or password", + Self::UserNotFound => "Email doesn’t exist. Register", + Self::UserExists => "An account already exists with this email", + Self::LinkInvalid => "Invalid or expired link", + Self::UnverifiedUser => "Kindly verify your account", + Self::InvalidOldPassword => "Old password incorrect. Please enter the correct password", + Self::EmailParsingError => "Invalid Email", + Self::NameParsingError => "Invalid Name", + Self::PasswordParsingError => "Invalid Password", + Self::UserAlreadyVerified => "User already verified", + Self::CompanyNameParsingError => "Invalid Company Name", + Self::MerchantAccountCreationError(error_message) => error_message, + Self::InvalidEmailError => "Invalid Email", + Self::MerchantIdNotFound => "Invalid Merchant ID", + Self::MetadataAlreadySet => "Metadata already set", + Self::DuplicateOrganizationId => "An Organization with the id already exists", + Self::InvalidRoleId => "Invalid Role ID", + Self::InvalidRoleOperation => "User Role Operation Not Supported", + Self::IpAddressParsingFailed => "Something went wrong", + Self::InvalidMetadataRequest => "Invalid Metadata Request", + Self::MerchantIdParsingError => "Invalid Merchant Id", + Self::ChangePasswordError => "Old and new password cannot be the same", + Self::InvalidDeleteOperation => "Delete Operation Not Supported", + Self::MaxInvitationsError => "Maximum invite count per request exceeded", } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 3384e2290097..c2ed78f86658 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,4 +1,4 @@ -use api_models::user as user_api; +use api_models::user::{self as user_api, InviteMultipleUserResponse}; use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; #[cfg(feature = "email")] use error_stack::IntoReport; @@ -9,7 +9,7 @@ use router_env::env; #[cfg(feature = "email")] use router_env::logger; -use super::errors::{UserErrors, UserResponse}; +use super::errors::{UserErrors, UserResponse, UserResult}; #[cfg(feature = "email")] use crate::services::email::types as email_types; use crate::{ @@ -407,6 +407,12 @@ pub async fn invite_user( .await .change_context(UserErrors::InternalServerError)?; + let invitation_status = if cfg!(feature = "email") { + UserStatus::InvitationSent + } else { + UserStatus::Active + }; + let now = common_utils::date_time::now(); state .store @@ -415,7 +421,7 @@ pub async fn invite_user( merchant_id: user_from_token.merchant_id, role_id: request.role_id, org_id: user_from_token.org_id, - status: UserStatus::InvitationSent, + status: invitation_status, created_by: user_from_token.user_id.clone(), last_modified_by: user_from_token.user_id, created_at: now, @@ -467,6 +473,181 @@ pub async fn invite_user( } } +pub async fn invite_multiple_user( + state: AppState, + user_from_token: auth::UserFromToken, + requests: Vec, +) -> UserResponse> { + if requests.len() > 10 { + return Err(UserErrors::MaxInvitationsError.into()) + .attach_printable("Number of invite requests must not exceed 10"); + } + + let responses = futures::future::join_all(requests.iter().map(|request| async { + match handle_invitation(&state, &user_from_token, request).await { + Ok(response) => response, + Err(error) => InviteMultipleUserResponse { + email: request.email.clone(), + is_email_sent: false, + password: None, + error: Some(error.current_context().get_error_message().to_string()), + }, + } + })) + .await; + + Ok(ApplicationResponse::Json(responses)) +} + +async fn handle_invitation( + state: &AppState, + user_from_token: &auth::UserFromToken, + request: &user_api::InviteUserRequest, +) -> UserResult { + let inviter_user = user_from_token.get_user(state.clone()).await?; + + if inviter_user.email == request.email { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User Inviting themself"); + } + + utils::user_role::validate_role_id(request.role_id.as_str())?; + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + let invitee_user = state + .store + .find_user_by_email(invitee_email.clone().get_secret().expose().as_str()) + .await; + + if let Ok(invitee_user) = invitee_user { + handle_existing_user_invitation(state, user_from_token, request, invitee_user.into()).await + } else if invitee_user + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + handle_new_user_invitation(state, user_from_token, request).await + } else { + Err(UserErrors::InternalServerError.into()) + } +} + +//TODO: send email +async fn handle_existing_user_invitation( + state: &AppState, + user_from_token: &auth::UserFromToken, + request: &user_api::InviteUserRequest, + invitee_user_from_db: domain::UserFromStorage, +) -> UserResult { + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: invitee_user_from_db.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id.clone(), + role_id: request.role_id.clone(), + org_id: user_from_token.org_id.clone(), + status: UserStatus::Active, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id.clone(), + created_at: now, + last_modified: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + Ok(InviteMultipleUserResponse { + email: request.email.clone(), + is_email_sent: false, + password: None, + error: None, + }) +} + +async fn handle_new_user_invitation( + state: &AppState, + user_from_token: &auth::UserFromToken, + request: &user_api::InviteUserRequest, +) -> UserResult { + let new_user = domain::NewUser::try_from((request.clone(), user_from_token.clone()))?; + + new_user + .insert_user_in_db(state.store.as_ref()) + .await + .change_context(UserErrors::InternalServerError)?; + + let invitation_status = if cfg!(feature = "email") { + UserStatus::InvitationSent + } else { + UserStatus::Active + }; + + let now = common_utils::date_time::now(); + state + .store + .insert_user_role(UserRoleNew { + user_id: new_user.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id.clone(), + role_id: request.role_id.clone(), + org_id: user_from_token.org_id.clone(), + status: invitation_status, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id.clone(), + created_at: now, + last_modified: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + let is_email_sent; + #[cfg(feature = "email")] + { + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + }; + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + logger::info!(?send_email_result); + is_email_sent = send_email_result.is_ok(); + } + #[cfg(not(feature = "email"))] + { + is_email_sent = false; + } + + Ok(InviteMultipleUserResponse { + is_email_sent, + password: if cfg!(not(feature = "email")) { + Some(new_user.get_password().get_secret()) + } else { + None + }, + email: request.email.clone(), + error: None, + }) +} + pub async fn create_internal_user( state: AppState, request: user_api::CreateInternalUserRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 71c79295c73d..44822efddc48 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -970,6 +970,9 @@ impl User { .service(web::resource("/user/invite").route(web::post().to(invite_user))) .service(web::resource("/user/invite/accept").route(web::post().to(accept_invitation))) .service(web::resource("/update").route(web::post().to(update_user_account_details))) + .service( + web::resource("/user/invite_multiple").route(web::post().to(invite_multiple_user)), + ) .service( web::resource("/data") .route(web::get().to(get_multiple_dashboard_metadata)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 30348513c2b7..6df8c7fb7a7c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -176,6 +176,7 @@ impl From for ApiIdentifier { | Flow::ForgotPassword | Flow::ResetPassword | Flow::InviteUser + | Flow::InviteMultipleUser | Flow::DeleteUser | Flow::UserSignUpWithMerchantId | Flow::VerifyEmail diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index eca32318adf6..02704cf701f5 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -350,6 +350,23 @@ pub async fn invite_user( )) .await } +pub async fn invite_multiple_user( + state: web::Data, + req: HttpRequest, + payload: web::Json>, +) -> HttpResponse { + let flow = Flow::InviteMultipleUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + user_core::invite_multiple_user, + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} #[cfg(feature = "email")] pub async fn verify_email( diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 84f2e3e12674..998c52f2c138 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -321,6 +321,8 @@ pub enum Flow { ResetPassword, /// Invite users InviteUser, + /// Invite multiple users + InviteMultipleUser, /// Delete user DeleteUser, /// Incremental Authorization flow From 6e3195a5aabe2796acae1805de5a61a637a19533 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:01:26 +0530 Subject: [PATCH 392/443] ci: update `create-hotfix-tag` and `create-hotfix-branch` workflows to handle CalVer tags (#3451) --- .github/workflows/create-hotfix-branch.yml | 8 ++++---- .github/workflows/create-hotfix-tag.yml | 10 +++++----- .github/workflows/release-nightly-version-reusable.yml | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml index 6fd2d4947719..e96508165fc4 100644 --- a/.github/workflows/create-hotfix-branch.yml +++ b/.github/workflows/create-hotfix-branch.yml @@ -25,17 +25,17 @@ jobs: - name: Check if the input is valid tag shell: bash run: | - if [[ ${{github.ref}} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::notice::${{github.ref}} is a valid tag." + if [[ ${{github.ref}} =~ ^refs/tags/[0-9]{4}\.[0-9]{2}\.[0-9]{2}.[0-9]+$ ]]; then + echo "::notice::${{github.ref}} is a CalVer tag." else - echo "::error::${{github.ref}} is not a valid tag." + echo "::error::${{github.ref}} is not a CalVer tag." exit 1 fi - name: Create hotfix branch shell: bash run: | - HOTFIX_BRANCH="hotfix-${GITHUB_REF#refs/tags/v}" + HOTFIX_BRANCH="hotfix-${GITHUB_REF#refs/tags/}" if git switch --create "$HOTFIX_BRANCH"; then git push origin "$HOTFIX_BRANCH" diff --git a/.github/workflows/create-hotfix-tag.yml b/.github/workflows/create-hotfix-tag.yml index e9df004139e0..8989094f1d21 100644 --- a/.github/workflows/create-hotfix-tag.yml +++ b/.github/workflows/create-hotfix-tag.yml @@ -31,10 +31,10 @@ jobs: - name: Check if the input is valid hotfix branch shell: bash run: | - if [[ ${{github.ref}} =~ ^refs/heads/hotfix-[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::notice::${{github.ref}} is a valid branch." + if [[ ${{github.ref}} =~ ^refs/heads/hotfix-[0-9]{4}\.[0-9]{2}\.[0-9]{2}.[0-9]+$ ]]; then + echo "::notice::${{github.ref}} is a valid hotfix branch." else - echo "::error::${{github.ref}} is not a valid branch." + echo "::error::${{github.ref}} is not a valid hotfix branch." exit 1 fi @@ -56,11 +56,11 @@ jobs: local previous_hotfix_number local next_tag - previous_hotfix_number="$(echo "${previous_tag}" | awk -F. '{ print $4 }')" + previous_hotfix_number="$(echo "${previous_tag}" | awk -F. '{ print $4 }' | gsed -oE 's/([0-9]+)(-hotfix([0-9]+))?/\3/')" if [[ -z "${previous_hotfix_number}" ]]; then # Previous tag was not a hotfix tag - next_tag="${previous_tag}+hotfix.1" + next_tag="${previous_tag}-hotfix1" else # Previous tag was a hotfix tag, increment hotfix number local hotfix_number=$((previous_hotfix_number + 1)) diff --git a/.github/workflows/release-nightly-version-reusable.yml b/.github/workflows/release-nightly-version-reusable.yml index accd8c12a913..f982a699895a 100644 --- a/.github/workflows/release-nightly-version-reusable.yml +++ b/.github/workflows/release-nightly-version-reusable.yml @@ -50,6 +50,11 @@ jobs: exit 1 fi + # Pulling latest changes in case pre-release steps push new commits + - name: Pull allowed branch + shell: bash + run: git pull + - name: Install Rust uses: dtolnay/rust-toolchain@master with: From 5ab44377b84941b8b59f9e73b1d1f0c3889eb02b Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:17:00 +0530 Subject: [PATCH 393/443] refactor(payouts): Propagate `Not Implemented` error (#3429) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/connector/wise.rs | 11 +++++++++++ crates/router/src/core/errors/utils.rs | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/crates/router/src/connector/wise.rs b/crates/router/src/connector/wise.rs index d2ec08c607c4..66ccf22c100d 100644 --- a/crates/router/src/connector/wise.rs +++ b/crates/router/src/connector/wise.rs @@ -589,6 +589,17 @@ impl types::PayoutsResponseData, > for Wise { + fn build_request( + &self, + _req: &types::PayoutsRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + // Eligiblity check for cards is not implemented + Err( + errors::ConnectorError::NotImplemented("Payout Eligibility for Wise".to_string()) + .into(), + ) + } } #[cfg(feature = "payouts")] diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index f00948b887e1..a54dae1c0352 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -461,6 +461,13 @@ impl ConnectorErrorExt for error_stack::Result message: format!("{} by {}", message, connector), } } + errors::ConnectorError::NotImplemented(reason) => { + errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + reason.to_string(), + ), + } + } _ => errors::ApiErrorResponse::InternalServerError, }; err.change_context(error) From 10055c1a7354faae8d0f504e0851d2046df5734a Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:46:47 +0530 Subject: [PATCH 394/443] chore(configs): [Cashtocode] wasm changes for CAD, CHF currency (#3461) --- crates/connector_configs/toml/development.toml | 16 ++++++++++++++++ crates/connector_configs/toml/production.toml | 16 ++++++++++++++++ crates/connector_configs/toml/sandbox.toml | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index dfa0a9ec9232..07003e89e61e 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -639,6 +639,22 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" [cashtocode.connector_webhook_details] merchant_secret="Source verification key" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index e837314f6106..7d4931f72d1e 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -517,6 +517,22 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" [cashtocode.connector_webhook_details] merchant_secret="Source verification key" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 47de5cd5d5ff..09883ead06ff 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -639,6 +639,22 @@ merchant_id_classic="MerchantId Classic" password_evoucher="Password Evoucher" username_evoucher="Username Evoucher" merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CAD.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.classic] +password_classic="Password Classic" +username_classic="Username Classic" +merchant_id_classic="MerchantId Classic" +[cashtocode.connector_auth.CurrencyAuthKey.auth_key_map.CHF.evoucher] +password_evoucher="Password Evoucher" +username_evoucher="Username Evoucher" +merchant_id_evoucher="MerchantId Evoucher" [cashtocode.connector_webhook_details] merchant_secret="Source verification key" From 66cd5b2fc9a32085608ed34e0af477dcafe4b957 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:34:52 +0530 Subject: [PATCH 395/443] refactor(connector): use utility function to raise payment method not implemented errors (#1871) Co-authored-by: Prasunna Soppa --- .../router/src/connector/boku/transformers.rs | 51 ++++++++++++++++--- .../src/connector/globepay/transformers.rs | 45 ++++++++++++++-- crates/router/src/connector/iatapay.rs | 18 ++++--- .../src/connector/iatapay/transformers.rs | 9 ++-- 4 files changed, 101 insertions(+), 22 deletions(-) diff --git a/crates/router/src/connector/boku/transformers.rs b/crates/router/src/connector/boku/transformers.rs index c671560765d0..6d830f85110c 100644 --- a/crates/router/src/connector/boku/transformers.rs +++ b/crates/router/src/connector/boku/transformers.rs @@ -6,7 +6,7 @@ use url::Url; use uuid::Uuid; use crate::{ - connector::utils::{AddressDetailsData, RouterData}, + connector::utils::{self, AddressDetailsData, RouterData}, core::errors, services::{self, RedirectForm}, types::{self, api, storage::enums}, @@ -81,10 +81,23 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BokuPaymentsRequest { api_models::payments::PaymentMethodData::Wallet(wallet_data) => { Self::try_from((item, &wallet_data)) } - _ => Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.request.payment_method_type), - connector: "Boku", - })?, + api_models::payments::PaymentMethodData::Card(_) + | api_models::payments::PaymentMethodData::CardRedirect(_) + | api_models::payments::PaymentMethodData::PayLater(_) + | api_models::payments::PaymentMethodData::BankRedirect(_) + | api_models::payments::PaymentMethodData::BankDebit(_) + | api_models::payments::PaymentMethodData::BankTransfer(_) + | api_models::payments::PaymentMethodData::Crypto(_) + | api_models::payments::PaymentMethodData::MandatePayment + | api_models::payments::PaymentMethodData::Reward + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::Voucher(_) + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("boku"), + ))? + } } } } @@ -136,9 +149,31 @@ fn get_wallet_type(wallet_data: &api::WalletData) -> Result { Ok(BokuPaymentType::Kakaopay.to_string()) } - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), - )), + api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::AliPayRedirect(_) + | api_models::payments::WalletData::AliPayHkRedirect(_) + | api_models::payments::WalletData::ApplePay(_) + | api_models::payments::WalletData::ApplePayRedirect(_) + | api_models::payments::WalletData::ApplePayThirdPartySdk(_) + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::GooglePayRedirect(_) + | api_models::payments::WalletData::GooglePayThirdPartySdk(_) + | api_models::payments::WalletData::MbWayRedirect(_) + | api_models::payments::WalletData::MobilePayRedirect(_) + | api_models::payments::WalletData::PaypalRedirect(_) + | api_models::payments::WalletData::PaypalSdk(_) + | api_models::payments::WalletData::SamsungPay(_) + | api_models::payments::WalletData::TwintRedirect {} + | api_models::payments::WalletData::VippsRedirect {} + | api_models::payments::WalletData::TouchNGoRedirect(_) + | api_models::payments::WalletData::WeChatPayRedirect(_) + | api_models::payments::WalletData::WeChatPayQr(_) + | api_models::payments::WalletData::CashappQr(_) + | api_models::payments::WalletData::SwishQr(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("boku"), + )) + } } } diff --git a/crates/router/src/connector/globepay/transformers.rs b/crates/router/src/connector/globepay/transformers.rs index f6adacb814de..a874fc21d297 100644 --- a/crates/router/src/connector/globepay/transformers.rs +++ b/crates/router/src/connector/globepay/transformers.rs @@ -3,7 +3,7 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::RouterData, + connector::utils::{self, RouterData}, consts, core::errors, types::{self, api, storage::enums}, @@ -31,12 +31,47 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for GlobepayPaymentsRequest { api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { api::WalletData::AliPayQr(_) => GlobepayChannel::Alipay, api::WalletData::WeChatPayQr(_) => GlobepayChannel::Wechat, - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), + api::WalletData::AliPayRedirect(_) + | api::WalletData::AliPayHkRedirect(_) + | api::WalletData::MomoRedirect(_) + | api::WalletData::KakaoPayRedirect(_) + | api::WalletData::GoPayRedirect(_) + | api::WalletData::GcashRedirect(_) + | api::WalletData::ApplePay(_) + | api::WalletData::ApplePayRedirect(_) + | api::WalletData::ApplePayThirdPartySdk(_) + | api::WalletData::DanaRedirect {} + | api::WalletData::GooglePay(_) + | api::WalletData::GooglePayRedirect(_) + | api::WalletData::GooglePayThirdPartySdk(_) + | api::WalletData::MbWayRedirect(_) + | api::WalletData::MobilePayRedirect(_) + | api::WalletData::PaypalRedirect(_) + | api::WalletData::PaypalSdk(_) + | api::WalletData::SamsungPay(_) + | api::WalletData::TwintRedirect {} + | api::WalletData::VippsRedirect {} + | api::WalletData::TouchNGoRedirect(_) + | api::WalletData::WeChatPayRedirect(_) + | api::WalletData::CashappQr(_) + | api::WalletData::SwishQr(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("globepay"), ))?, }, - _ => Err(errors::ConnectorError::NotImplemented( - "Payment method".to_string(), + api::PaymentMethodData::Card(_) + | api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("globepay"), ))?, }; let description = item.get_description()?; diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 72d7b70b061d..c5ac0f43833c 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -685,12 +685,18 @@ impl api::IncomingWebhook for Iatapay { &self, request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - let notif: IatapayPaymentsResponse = - request - .body - .parse_struct("IatapayPaymentsResponse") - .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let notif: iatapay::IatapayWebhookResponse = request + .body + .parse_struct("IatapayWebhookResponse") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - Ok(Box::new(notif)) + match notif { + iatapay::IatapayWebhookResponse::IatapayPaymentWebhookBody(wh_body) => { + Ok(Box::new(wh_body)) + } + iatapay::IatapayWebhookResponse::IatapayRefundWebhookBody(refund_wh_body) => { + Ok(Box::new(refund_wh_body)) + } + } } } diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 14b37d1418d1..4557fda03904 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -265,9 +265,6 @@ pub struct IatapayPaymentsResponse { pub merchant_payment_id: Option, pub amount: f64, pub currency: String, - pub country: Option, - pub locale: Option, - pub bank_transfer_description: Option, pub checkout_methods: Option, pub failure_code: Option, pub failure_details: Option, @@ -532,6 +529,9 @@ pub struct IatapayPaymentWebhookBody { pub merchant_payment_id: Option, pub failure_code: Option, pub failure_details: Option, + pub amount: f64, + pub currency: String, + pub checkout_methods: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -542,9 +542,12 @@ pub struct IatapayRefundWebhookBody { pub merchant_refund_id: Option, pub failure_code: Option, pub failure_details: Option, + pub amount: f64, + pub currency: String, } #[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] pub enum IatapayWebhookResponse { IatapayPaymentWebhookBody(IatapayPaymentWebhookBody), IatapayRefundWebhookBody(IatapayRefundWebhookBody), From 98bf8f5b85d318cd6ad3c7105fdde3149a2ef8ed Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Sun, 28 Jan 2024 20:37:52 +0530 Subject: [PATCH 396/443] ci: fix `create-hot-fix-tag` workflow to filter for `CalVer` tags (#3463) --- .github/workflows/create-hotfix-branch.yml | 2 +- .github/workflows/create-hotfix-tag.yml | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml index e96508165fc4..d7afb388f2f8 100644 --- a/.github/workflows/create-hotfix-branch.yml +++ b/.github/workflows/create-hotfix-branch.yml @@ -25,7 +25,7 @@ jobs: - name: Check if the input is valid tag shell: bash run: | - if [[ ${{github.ref}} =~ ^refs/tags/[0-9]{4}\.[0-9]{2}\.[0-9]{2}.[0-9]+$ ]]; then + if [[ ${{github.ref}} =~ ^refs/tags/[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$ ]]; then echo "::notice::${{github.ref}} is a CalVer tag." else echo "::error::${{github.ref}} is not a CalVer tag." diff --git a/.github/workflows/create-hotfix-tag.yml b/.github/workflows/create-hotfix-tag.yml index 8989094f1d21..2250ce7ece59 100644 --- a/.github/workflows/create-hotfix-tag.yml +++ b/.github/workflows/create-hotfix-tag.yml @@ -31,7 +31,7 @@ jobs: - name: Check if the input is valid hotfix branch shell: bash run: | - if [[ ${{github.ref}} =~ ^refs/heads/hotfix-[0-9]{4}\.[0-9]{2}\.[0-9]{2}.[0-9]+$ ]]; then + if [[ ${{github.ref}} =~ ^refs/heads/hotfix-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$ ]]; then echo "::notice::${{github.ref}} is a valid hotfix branch." else echo "::error::${{github.ref}} is not a valid hotfix branch." @@ -56,7 +56,7 @@ jobs: local previous_hotfix_number local next_tag - previous_hotfix_number="$(echo "${previous_tag}" | awk -F. '{ print $4 }' | gsed -oE 's/([0-9]+)(-hotfix([0-9]+))?/\3/')" + previous_hotfix_number="$(echo "${previous_tag}" | awk -F. '{ print $4 }' | sed -E 's/([0-9]+)(-hotfix([0-9]+))?/\3/')" if [[ -z "${previous_hotfix_number}" ]]; then # Previous tag was not a hotfix tag @@ -70,7 +70,13 @@ jobs: echo "${next_tag}" } - PREVIOUS_TAG="$(git tag --merged | sort --version-sort | tail --lines 1)" + # Search for date-like tags (no strict checking), sort and obtain previous tag + PREVIOUS_TAG="$( + git tag --merged \ + | grep --extended-regexp '[0-9]{4}\.[0-9]{2}\.[0-9]{2}' \ + | sort --version-sort \ + | tail --lines 1 + )" NEXT_TAG="$(get_next_tag "${PREVIOUS_TAG}")" echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_ENV From 0e4e18441d024b7d669b27d6f8a2feb3eccedb2a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 00:19:16 +0000 Subject: [PATCH 397/443] chore(version): 2024.01.29.0 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 071f60d224f4..c6d7a952adc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.29.0 + +### Features + +- **connector:** [Adyen] Add support for PIX Payment Method ([#3236](https://github.com/juspay/hyperswitch/pull/3236)) ([`fc6e68f`](https://github.com/juspay/hyperswitch/commit/fc6e68f7f07bf2d48466fa493596c0db02d7550a)) +- **core:** + - [CYBERSOURCE] Add original authorized amount in router data ([#3417](https://github.com/juspay/hyperswitch/pull/3417)) ([`47fbe48`](https://github.com/juspay/hyperswitch/commit/47fbe486cec252b8befca38f1b7ea77cc0823ee5)) + - Add outgoing webhook for manual `partial_capture` events ([#3388](https://github.com/juspay/hyperswitch/pull/3388)) ([`d5e9866`](https://github.com/juspay/hyperswitch/commit/d5e9866b522bad3e62f6f6c0d7993f5dcc2939af)) +- **logging:** Add a logging middleware to log all api requests ([#3437](https://github.com/juspay/hyperswitch/pull/3437)) ([`c2946cf`](https://github.com/juspay/hyperswitch/commit/c2946cfe05ffa81a66643e04eff5e89b545d2d43)) +- **user:** + - Add support to delete user ([#3374](https://github.com/juspay/hyperswitch/pull/3374)) ([`7777710`](https://github.com/juspay/hyperswitch/commit/777771048a8144aac9e2f837c85531e139ecc125)) + - Support multiple invites ([#3422](https://github.com/juspay/hyperswitch/pull/3422)) ([`a59ac7d`](https://github.com/juspay/hyperswitch/commit/a59ac7d5b98f27f5fb34206c20ef9c37a07259a3)) + +### Bug Fixes + +- **connector:** + - Use `ConnectorError::InvalidConnectorConfig` for an invalid `CoinbaseConnectorMeta` ([#3168](https://github.com/juspay/hyperswitch/pull/3168)) ([`d827c9a`](https://github.com/juspay/hyperswitch/commit/d827c9af29b8516f379e648e00f4ab307ae1a34d)) + - Fix connector template script ([#3453](https://github.com/juspay/hyperswitch/pull/3453)) ([`9a54838`](https://github.com/juspay/hyperswitch/commit/9a54838b0529013ab8f449ec6b347a104b55f8f7)) + - [HELCIM] Handle 4XX Errors ([#3458](https://github.com/juspay/hyperswitch/pull/3458)) ([`ec859ea`](https://github.com/juspay/hyperswitch/commit/ec859eabbfb8a511f0fffd30a47a144fb07f2886)) +- **core:** Return surcharge in payment method list response if passed in create request ([#3363](https://github.com/juspay/hyperswitch/pull/3363)) ([`3507ad6`](https://github.com/juspay/hyperswitch/commit/3507ad60b2f1fd84d32eb4d97fe0a847db6f2045)) +- **euclid_wasm:** Include `payouts` feature in `default` features ([#3392](https://github.com/juspay/hyperswitch/pull/3392)) ([`b45e4ca`](https://github.com/juspay/hyperswitch/commit/b45e4ca2a3788823701bdeac2e2a8c1147bb071a)) + +### Refactors + +- **connector:** + - [Iatapay] refactor authorize flow and fix payment status mapping ([#2409](https://github.com/juspay/hyperswitch/pull/2409)) ([`f0c7bb9`](https://github.com/juspay/hyperswitch/commit/f0c7bb9a5228f2ee31858fea07abe4ecee9b78a2)) + - Use utility function to raise payment method not implemented errors ([#1871](https://github.com/juspay/hyperswitch/pull/1871)) ([`66cd5b2`](https://github.com/juspay/hyperswitch/commit/66cd5b2fc9a32085608ed34e0af477dcafe4b957)) +- **payouts:** Propagate `Not Implemented` error ([#3429](https://github.com/juspay/hyperswitch/pull/3429)) ([`5ab4437`](https://github.com/juspay/hyperswitch/commit/5ab44377b84941b8b59f9e73b1d1f0c3889eb02b)) + +### Miscellaneous Tasks + +- **configs:** [Cashtocode] wasm changes for CAD, CHF currency ([#3461](https://github.com/juspay/hyperswitch/pull/3461)) ([`10055c1`](https://github.com/juspay/hyperswitch/commit/10055c1a7354faae8d0f504e0851d2046df5734a)) + +**Full Changelog:** [`2024.01.25.0...2024.01.29.0`](https://github.com/juspay/hyperswitch/compare/2024.01.25.0...2024.01.29.0) + +- - - + ## 2024.01.25.0 ### Refactors From dd0d2dc2dd9a6263bbb8a99d1f0b2077f38dd621 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:30:35 +0530 Subject: [PATCH 398/443] feat(router): add request_details logger middleware for 400 bad requests (#3414) Co-authored-by: Sampras lopes Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/Cargo.toml | 1 + crates/router/src/lib.rs | 2 + crates/router/src/middleware.rs | 117 +++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index ef6ea41d524a..2c3ef2911c22 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -124,6 +124,7 @@ storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = erased-serde = "0.3.31" quick-xml = { version = "0.31.0", features = ["serialize"] } rdkafka = "0.36.0" +actix-http = "3.3.1" [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 7b1aba14106f..ee22e40190b0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -268,5 +268,7 @@ pub fn get_application_builder( .wrap(middleware::RequestId) .wrap(cors::cors()) .wrap(middleware::LogSpanInitializer) + // this middleware works only for Http1.1 requests + .wrap(middleware::Http400RequestDetailsLogger) .wrap(router_env::tracing_actix_web::TracingLogger::default()) } diff --git a/crates/router/src/middleware.rs b/crates/router/src/middleware.rs index 1feba66a34f8..587a15693f2e 100644 --- a/crates/router/src/middleware.rs +++ b/crates/router/src/middleware.rs @@ -1,4 +1,8 @@ -use router_env::tracing::{field::Empty, Instrument}; +use futures::StreamExt; +use router_env::{ + logger, + tracing::{field::Empty, Instrument}, +}; /// Middleware to include request ID in response header. pub struct RequestId; @@ -149,3 +153,114 @@ where ) } } + +fn get_request_details_from_value(json_value: &serde_json::Value, parent_key: &str) -> String { + match json_value { + serde_json::Value::Null => format!("{}: null", parent_key), + serde_json::Value::Bool(b) => format!("{}: {}", parent_key, b), + serde_json::Value::Number(num) => format!("{}: {}", parent_key, num.to_string().len()), + serde_json::Value::String(s) => format!("{}: {}", parent_key, s.len()), + serde_json::Value::Array(arr) => { + let mut result = String::new(); + for (index, value) in arr.iter().enumerate() { + let child_key = format!("{}[{}]", parent_key, index); + result.push_str(&get_request_details_from_value(value, &child_key)); + if index < arr.len() - 1 { + result.push_str(", "); + } + } + result + } + serde_json::Value::Object(obj) => { + let mut result = String::new(); + for (index, (key, value)) in obj.iter().enumerate() { + let child_key = format!("{}[{}]", parent_key, key); + result.push_str(&get_request_details_from_value(value, &child_key)); + if index < obj.len() - 1 { + result.push_str(", "); + } + } + result + } + } +} + +/// Middleware for Logging request_details of HTTP 400 Bad Requests +pub struct Http400RequestDetailsLogger; + +impl actix_web::dev::Transform + for Http400RequestDetailsLogger +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + >, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Transform = Http400RequestDetailsLoggerMiddleware; + type InitError = (); + type Future = std::future::Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + std::future::ready(Ok(Http400RequestDetailsLoggerMiddleware { + service: std::rc::Rc::new(service), + })) + } +} + +pub struct Http400RequestDetailsLoggerMiddleware { + service: std::rc::Rc, +} + +impl actix_web::dev::Service + for Http400RequestDetailsLoggerMiddleware +where + S: actix_web::dev::Service< + actix_web::dev::ServiceRequest, + Response = actix_web::dev::ServiceResponse, + Error = actix_web::Error, + > + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = actix_web::dev::ServiceResponse; + type Error = actix_web::Error; + type Future = futures::future::LocalBoxFuture<'static, Result>; + + actix_web::dev::forward_ready!(service); + + fn call(&self, mut req: actix_web::dev::ServiceRequest) -> Self::Future { + let svc = self.service.clone(); + let request_id_fut = req.extract::(); + Box::pin(async move { + let (http_req, payload) = req.into_parts(); + let result_payload: Vec> = + payload.collect().await; + let payload = result_payload + .into_iter() + .collect::, actix_web::error::PayloadError>>()?; + let bytes = payload.clone().concat().to_vec(); + // we are creating h1 payload manually from bytes, currently there's no way to create http2 payload with actix + let (_, mut new_payload) = actix_http::h1::Payload::create(true); + new_payload.unread_data(bytes.to_vec().clone().into()); + let new_req = actix_web::dev::ServiceRequest::from_parts(http_req, new_payload.into()); + let response_fut = svc.call(new_req); + let response = response_fut.await?; + // Log the request_details when we receive 400 status from the application + if response.status() == 400 { + let value: serde_json::Value = serde_json::from_slice(&bytes)?; + let request_id = request_id_fut.await?.as_hyphenated().to_string(); + logger::info!( + "request_id: {}, request_details: {}", + request_id, + get_request_details_from_value(&value, "") + ); + } + Ok(response) + }) + } +} From 7d8d68faba55dfcb2886c63ae7969ebd4b9ec98c Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:20:43 +0530 Subject: [PATCH 399/443] refactor(openapi): move openapi to separate crate to decrease compile times (#3110) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Co-authored-by: Sahkal Poddar Co-authored-by: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Co-authored-by: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Co-authored-by: shashank_attarde Co-authored-by: Aprabhat19 Co-authored-by: sai-harsha-vardhan Co-authored-by: Sahkal Poddar Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- .github/workflows/validate-openapi-spec.yml | 2 +- Cargo.lock | 84 +- crates/api_models/Cargo.toml | 3 +- crates/api_models/src/admin.rs | 238 +- crates/api_models/src/enums.rs | 3 +- crates/api_models/src/gsm.rs | 47 + crates/api_models/src/payment_methods.rs | 6 +- crates/api_models/src/payments.rs | 163 +- crates/api_models/src/refunds.rs | 25 +- crates/api_models/src/routing.rs | 43 +- crates/api_models/src/webhooks.rs | 8 +- crates/common_enums/Cargo.toml | 1 + crates/common_enums/src/enums.rs | 11 +- crates/common_utils/src/macros.rs | 12 + crates/euclid/Cargo.toml | 1 + crates/euclid/src/frontend/ast.rs | 53 +- crates/openapi/Cargo.toml | 14 + crates/openapi/src/lib.rs | 2 + crates/openapi/src/main.rs | 15 + crates/{router => openapi}/src/openapi.rs | 369 +- crates/openapi/src/routes.rs | 26 + crates/openapi/src/routes/api_keys.rs | 80 + crates/openapi/src/routes/blocklist.rs | 43 + crates/openapi/src/routes/business_profile.rs | 124 + crates/openapi/src/routes/customers.rs | 102 + crates/openapi/src/routes/disputes.rs | 44 + crates/openapi/src/routes/gsm.rs | 75 + crates/openapi/src/routes/mandates.rs | 61 + crates/openapi/src/routes/merchant_account.rs | 125 + .../src/routes/merchant_connector_account.rs | 170 + crates/openapi/src/routes/payment_link.rs | 19 + crates/openapi/src/routes/payment_method.rs | 173 + crates/openapi/src/routes/payments.rs | 452 + crates/openapi/src/routes/payouts.rs | 85 + crates/openapi/src/routes/refunds.rs | 145 + crates/openapi/src/routes/routing.rs | 202 + crates/router/Cargo.toml | 5 +- crates/router/src/bin/router.rs | 18 - crates/router/src/lib.rs | 11 +- crates/router/src/routes/app.rs | 2 +- crates/router/src/routes/customers.rs | 77 +- crates/router/src/routes/mandates.rs | 6 +- crates/router/src/routes/payment_methods.rs | 95 +- crates/router/src/routes/payments.rs | 8 +- crates/router/src/routes/routing.rs | 2 +- crates/router_derive/src/lib.rs | 5 +- .../src/macros/generate_schema.rs | 186 +- openapi/openapi_spec.json | 16440 ++++++++++------ 48 files changed, 12652 insertions(+), 7229 deletions(-) create mode 100644 crates/openapi/Cargo.toml create mode 100644 crates/openapi/src/lib.rs create mode 100644 crates/openapi/src/main.rs rename crates/{router => openapi}/src/openapi.rs (65%) create mode 100644 crates/openapi/src/routes.rs create mode 100644 crates/openapi/src/routes/api_keys.rs create mode 100644 crates/openapi/src/routes/blocklist.rs create mode 100644 crates/openapi/src/routes/business_profile.rs create mode 100644 crates/openapi/src/routes/customers.rs create mode 100644 crates/openapi/src/routes/disputes.rs create mode 100644 crates/openapi/src/routes/gsm.rs create mode 100644 crates/openapi/src/routes/mandates.rs create mode 100644 crates/openapi/src/routes/merchant_account.rs create mode 100644 crates/openapi/src/routes/merchant_connector_account.rs create mode 100644 crates/openapi/src/routes/payment_link.rs create mode 100644 crates/openapi/src/routes/payment_method.rs create mode 100644 crates/openapi/src/routes/payments.rs create mode 100644 crates/openapi/src/routes/payouts.rs create mode 100644 crates/openapi/src/routes/refunds.rs create mode 100644 crates/openapi/src/routes/routing.rs diff --git a/.github/workflows/validate-openapi-spec.yml b/.github/workflows/validate-openapi-spec.yml index bdb987d625ac..210f82064832 100644 --- a/.github/workflows/validate-openapi-spec.yml +++ b/.github/workflows/validate-openapi-spec.yml @@ -52,7 +52,7 @@ jobs: - name: Generate the OpenAPI spec file shell: bash - run: cargo run --features openapi -- generate-openapi-spec + run: cargo run -p openapi - name: Install `swagger-cli` shell: bash diff --git a/Cargo.lock b/Cargo.lock index 5623fd9f729f..f920b1ea9c53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2435,6 +2435,7 @@ dependencies = [ "serde_json", "strum 0.25.0", "thiserror", + "utoipa", ] [[package]] @@ -4043,6 +4044,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openapi" +version = "0.1.0" +dependencies = [ + "api_models", + "serde_json", + "utoipa", +] + [[package]] name = "openssl" version = "0.10.60" @@ -5185,6 +5195,7 @@ dependencies = [ "nanoid", "num_cpus", "once_cell", + "openapi", "openssl", "pm_auth", "qrcode", @@ -5222,7 +5233,6 @@ dependencies = [ "unicode-segmentation", "url", "utoipa", - "utoipa-swagger-ui", "uuid", "validator", "wiremock", @@ -5279,41 +5289,6 @@ dependencies = [ "xmlparser", ] -[[package]] -name = "rust-embed" -version = "6.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "6.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "shellexpand", - "syn 2.0.48", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "7.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" -dependencies = [ - "sha2", - "walkdir", -] - [[package]] name = "rust-ini" version = "0.18.0" @@ -5866,15 +5841,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs", -] - [[package]] name = "signal-hook" version = "0.3.17" @@ -7237,22 +7203,6 @@ dependencies = [ "syn 2.0.48", ] -[[package]] -name = "utoipa-swagger-ui" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" -dependencies = [ - "actix-web", - "mime_guess", - "regex", - "rust-embed", - "serde", - "serde_json", - "utoipa", - "zip", -] - [[package]] name = "uuid" version = "1.4.1" @@ -7820,18 +7770,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils 0.8.16", - "flate2", -] - [[package]] name = "zstd" version = "0.12.4" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index d1f603f188eb..8cd3ee53f218 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] frm = [] +openapi = ["common_enums/openapi"] recon = [] [dependencies] @@ -39,5 +40,5 @@ cards = { version = "0.1.0", path = "../cards" } common_enums = { path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } euclid = { version = "0.1.0", path = "../euclid" } -masking = { version = "0.1.0", path = "../masking" } +masking = { version = "0.1.0", path = "../masking", default-features = false, features = ["alloc", "serde"] } router_derive = { version = "0.1.0", path = "../router_derive" } diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 134beacd226f..68ee6cb48b74 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -32,7 +32,7 @@ pub struct MerchantAccountCreate { #[schema(value_type= Option,example = "NewAge Retailer")] pub merchant_name: Option>, - /// Merchant related details + /// Details about the merchant pub merchant_details: Option, /// The URL to redirect after the completion of the operation @@ -43,7 +43,8 @@ pub struct MerchantAccountCreate { pub webhook_details: Option, /// The routing algorithm to be used for routing payments to desired connectors - #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + #[serde(skip)] + #[schema(deprecated)] pub routing_algorithm: Option, /// The routing algorithm to be used for routing payouts to desired connectors @@ -67,8 +68,7 @@ pub struct MerchantAccountCreate { #[schema(default = false, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -87,7 +87,7 @@ pub struct MerchantAccountCreate { #[schema(example = "locker_abc123")] pub locker_id: Option, - ///Default business details for connector routing + /// Details about the primary business unit of the merchant account #[schema(value_type = Option)] pub primary_business_details: Option>, @@ -117,7 +117,7 @@ pub struct MerchantAccountUpdate { #[schema(example = "NewAge Retailer")] pub merchant_name: Option, - /// Merchant related details + /// Details about the merchant pub merchant_details: Option, /// The URL to redirect after the completion of the operation @@ -128,10 +128,11 @@ pub struct MerchantAccountUpdate { pub webhook_details: Option, /// The routing algorithm to be used for routing payments to desired connectors - #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + #[serde(skip)] + #[schema(deprecated)] pub routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -152,7 +153,7 @@ pub struct MerchantAccountUpdate { #[schema(default = false, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for payment response + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -171,7 +172,7 @@ pub struct MerchantAccountUpdate { #[schema(example = "locker_abc123")] pub locker_id: Option, - ///Default business details for connector routing + /// Details about the primary business unit of the merchant account pub primary_business_details: Option>, /// The frm routing algorithm to be used for routing payments to desired FRM's @@ -202,7 +203,7 @@ pub struct MerchantAccountResponse { #[schema(default = false, example = true)] pub enable_payment_response_hash: bool, - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. #[schema(max_length = 255, example = "xkkdf909012sdjki2dkh5sdf")] pub payment_response_hash_key: Option, @@ -210,7 +211,7 @@ pub struct MerchantAccountResponse { #[schema(default = false, example = true)] pub redirect_to_merchant_with_http_post: bool, - /// Merchant related details + /// Details about the merchant #[schema(value_type = Option)] pub merchant_details: Option>, @@ -219,10 +220,11 @@ pub struct MerchantAccountResponse { pub webhook_details: Option, /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' - #[schema(value_type = Option, max_length = 255, example = "custom")] + #[serde(skip)] + #[schema(deprecated)] pub routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -250,7 +252,8 @@ pub struct MerchantAccountResponse { /// An identifier for the vault used to store payment method information. #[schema(example = "locker_abc123")] pub locker_id: Option, - ///Default business details for connector routing + + /// Details about the primary business unit of the merchant account #[schema(value_type = Vec)] pub primary_business_details: Vec, @@ -272,7 +275,7 @@ pub struct MerchantAccountResponse { #[schema(max_length = 64)] pub default_profile: Option, - /// A enum value to indicate the status of recon service. By default it is not_requested. + /// Used to indicate the status of the recon module for a merchant account #[schema(value_type = ReconStatus, example = "not_requested")] pub recon_status: enums::ReconStatus, } @@ -508,23 +511,18 @@ pub struct MerchantConnectorCreate { /// Name of the Connector #[schema(value_type = Connector, example = "stripe")] pub connector_name: api_enums::Connector, - /// Connector label for a connector, this can serve as a field to identify the connector as per business details + /// This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default` #[schema(example = "stripe_US_travel")] pub connector_label: Option, - /// Unique ID of the connector - #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] - pub merchant_connector_id: Option, - /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] + /// Identifier for the business profile, if not provided default will be chosen from merchant account + pub profile_id: Option, + + /// An object containing the required details/credentials for a Connector account. + #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: Option, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. - #[schema(default = false, example = false)] - pub test_mode: Option, - /// A boolean value to indicate if the connector is disabled. By default, its value is false. - #[schema(default = false, example = false)] - pub disabled: Option, - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + + /// An object containing the details about the payment methods that need to be enabled under this merchant connector account #[schema(example = json!([ { "payment_method": "wallet", @@ -555,35 +553,49 @@ pub struct MerchantConnectorCreate { } ]))] pub payment_methods_enabled: Option>, + + /// Webhook details of this merchant connector + #[schema(example = json!({ + "connector_webhook_details": { + "merchant_secret": "1234567890987654321" + } + }))] + pub connector_webhook_details: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, - /// contains the frm configs for the merchant connector + + /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. + #[schema(default = false, example = false)] + pub test_mode: Option, + + /// A boolean value to indicate if the connector is disabled. By default, its value is false. + #[schema(default = false, example = false)] + pub disabled: Option, + + /// Contains the frm configs for the merchant connector #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] pub frm_configs: Option>, + /// The business country to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(value_type = Option, example = "US")] pub business_country: Option, + /// The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead pub business_label: Option, - /// Business Sub label of the merchant + /// The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(example = "chase")] pub business_sub_label: Option, - /// Webhook details of this merchant connector - #[schema(example = json!({ - "connector_webhook_details": { - "merchant_secret": "1234567890987654321" - } - }))] - pub connector_webhook_details: Option, - /// Identifier for the business profile, if not provided default will be chosen from merchant account - pub profile_id: Option, + /// Unique ID of the connector + #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] + pub merchant_connector_id: Option, pub pm_auth_config: Option, - #[schema(value_type = ConnectorStatus, example = "inactive")] + #[schema(value_type = Option, example = "inactive")] pub status: Option, } @@ -634,26 +646,26 @@ pub struct MerchantConnectorResponse { #[schema(value_type = ConnectorType, example = "payment_processor")] pub connector_type: api_enums::ConnectorType, /// Name of the Connector - #[schema(example = "stripe")] + #[schema(value_type = Connector, example = "stripe")] pub connector_name: String, - /// Connector label for a connector, this can serve as a field to identify the connector as per business details + /// A unique label to identify the connector account created under a business profile #[schema(example = "stripe_US_travel")] pub connector_label: Option, - /// Unique ID of the connector + /// Unique ID of the merchant connector account #[schema(example = "mca_5apGeP94tMts6rg3U3kR")] pub merchant_connector_id: String, - /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] + + /// Identifier for the business profile, if not provided default will be chosen from merchant account + #[schema(max_length = 64)] + pub profile_id: Option, + + /// An object containing the required details/credentials for a Connector account. + #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: pii::SecretSerdeValue, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. - #[schema(default = false, example = false)] - pub test_mode: Option, - /// A boolean value to indicate if the connector is disabled. By default, its value is false. - #[schema(default = false, example = false)] - pub disabled: Option, - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + + /// An object containing the details about the payment methods that need to be enabled under this merchant connector account #[schema(example = json!([ { "payment_method": "wallet", @@ -684,38 +696,43 @@ pub struct MerchantConnectorResponse { } ]))] pub payment_methods_enabled: Option>, + + /// Webhook details of this merchant connector + #[schema(example = json!({ + "connector_webhook_details": { + "merchant_secret": "1234567890987654321" + } + }))] + pub connector_webhook_details: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, - /// Business Country of the connector + /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. + #[schema(default = false, example = false)] + pub test_mode: Option, + + /// A boolean value to indicate if the connector is disabled. By default, its value is false. + #[schema(default = false, example = false)] + pub disabled: Option, + + /// Contains the frm configs for the merchant connector + #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] + pub frm_configs: Option>, + + /// The business country to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(value_type = Option, example = "US")] pub business_country: Option, - ///Business Type of the merchant + ///The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(example = "travel")] pub business_label: Option, - /// Business Sub label of the merchant + /// The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead #[schema(example = "chase")] pub business_sub_label: Option, - /// contains the frm configs for the merchant connector - #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] - pub frm_configs: Option>, - - /// Webhook details of this merchant connector - #[schema(example = json!({ - "connector_webhook_details": { - "merchant_secret": "1234567890987654321" - } - }))] - pub connector_webhook_details: Option, - - /// The business profile this connector must be created in - /// default value from merchant account is taken if not passed - #[schema(max_length = 64)] - pub profile_id: Option, /// identifier for the verified domains of a particular connector account pub applepay_verified_domains: Option>, @@ -733,22 +750,15 @@ pub struct MerchantConnectorUpdate { #[schema(value_type = ConnectorType, example = "payment_processor")] pub connector_type: api_enums::ConnectorType, - /// Connector label for a connector, this can serve as a field to identify the connector as per business details + /// This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default` + #[schema(example = "stripe_US_travel")] pub connector_label: Option, - /// Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object. - #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] + /// An object containing the required details/credentials for a Connector account. + #[schema(value_type = Option,example = json!({ "auth_type": "HeaderKey","api_key": "Basic MyVerySecretApiKey" }))] pub connector_account_details: Option, - /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. - #[schema(default = false, example = false)] - pub test_mode: Option, - - /// A boolean value to indicate if the connector is disabled. By default, its value is false. - #[schema(default = false, example = false)] - pub disabled: Option, - - /// Refers to the Parent Merchant ID if the merchant being created is a sub-merchant + /// An object containing the details about the payment methods that need to be enabled under this merchant connector account #[schema(example = json!([ { "payment_method": "wallet", @@ -780,14 +790,6 @@ pub struct MerchantConnectorUpdate { ]))] pub payment_methods_enabled: Option>, - /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. - #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] - pub metadata: Option, - - /// contains the frm configs for the merchant connector - #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] - pub frm_configs: Option>, - /// Webhook details of this merchant connector #[schema(example = json!({ "connector_webhook_details": { @@ -796,6 +798,22 @@ pub struct MerchantConnectorUpdate { }))] pub connector_webhook_details: Option, + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option,max_length = 255,example = json!({ "city": "NY", "unit": "245" }))] + pub metadata: Option, + + /// A boolean value to indicate if the connector is in Test mode. By default, its value is false. + #[schema(default = false, example = false)] + pub test_mode: Option, + + /// A boolean value to indicate if the connector is disabled. By default, its value is false. + #[schema(default = false, example = false)] + pub disabled: Option, + + /// Contains the frm configs for the merchant connector + #[schema(example = json!(common_utils::consts::FRM_CONFIGS_EG))] + pub frm_configs: Option>, + pub pm_auth_config: Option, #[schema(value_type = ConnectorStatus, example = "inactive")] @@ -876,6 +894,7 @@ pub enum AcceptedCurrencies { content = "list", rename_all = "snake_case" )] +/// Object to filter the customer countries for which the payment method is displayed pub enum AcceptedCountries { #[schema(value_type = Vec)] EnableOnly(Vec), @@ -961,12 +980,11 @@ pub enum PayoutStraightThroughAlgorithm { #[derive(Clone, Debug, Deserialize, ToSchema, Default, Serialize)] #[serde(deny_unknown_fields)] pub struct BusinessProfileCreate { - /// A short name to identify the business profile + /// The name of business profile #[schema(max_length = 64)] pub profile_name: Option, - /// The URL to redirect after the completion of the operation, This will be applied to all the - /// connector accounts under this profile + /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, @@ -974,8 +992,7 @@ pub struct BusinessProfileCreate { #[schema(default = true, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -1002,7 +1019,7 @@ pub struct BusinessProfileCreate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -1028,16 +1045,15 @@ pub struct BusinessProfileResponse { #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")] pub merchant_id: String, - /// The unique identifier for Business Profile + /// The default business profile that must be used for creating merchant accounts and payments #[schema(max_length = 64, example = "pro_abcdefghijklmnopqrstuvwxyz")] pub profile_id: String, - /// A short name to identify the business profile + /// Name of the business profile #[schema(max_length = 64)] pub profile_name: String, - /// The URL to redirect after the completion of the operation, This will be applied to all the - /// connector accounts under this profile + /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, @@ -1045,8 +1061,7 @@ pub struct BusinessProfileResponse { #[schema(default = true, example = true)] pub enable_payment_response_hash: bool, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -1054,6 +1069,7 @@ pub struct BusinessProfileResponse { pub redirect_to_merchant_with_http_post: bool, /// Webhook related details + #[schema(value_type = Option)] pub webhook_details: Option, /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. @@ -1069,11 +1085,11 @@ pub struct BusinessProfileResponse { #[schema(example = 900)] pub intent_fulfillment_time: Option, - /// The frm routing algorithm to be used for routing payments to desired FRM's + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( @@ -1096,12 +1112,11 @@ pub struct BusinessProfileResponse { #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] pub struct BusinessProfileUpdate { - /// A short name to identify the business profile + /// The name of business profile #[schema(max_length = 64)] pub profile_name: Option, - /// The URL to redirect after the completion of the operation, This will be applied to all the - /// connector accounts under this profile + /// The URL to redirect after the completion of the operation #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, @@ -1109,8 +1124,7 @@ pub struct BusinessProfileUpdate { #[schema(default = true, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for calculating the signature for webhooks and redirect response - /// If the value is not provided, a default value is used + /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -1137,7 +1151,7 @@ pub struct BusinessProfileUpdate { #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] pub frm_routing_algorithm: Option, - /// The routing algorithm to be used for routing payouts to desired connectors + /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] #[serde( diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 558223a68eed..cc2052d18a99 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -13,11 +13,9 @@ use utoipa::ToSchema; serde::Serialize, strum::Display, strum::EnumString, - ToSchema, )] /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' -#[schema(example = "custom")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithm { @@ -27,6 +25,7 @@ pub enum RoutingAlgorithm { Custom, } +/// A connector is an integration to fulfill payments #[derive( Clone, Copy, diff --git a/crates/api_models/src/gsm.rs b/crates/api_models/src/gsm.rs index 81798d05178b..30e49062439f 100644 --- a/crates/api_models/src/gsm.rs +++ b/crates/api_models/src/gsm.rs @@ -4,25 +4,41 @@ use crate::enums::Connector; #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmCreateRequest { + /// The connector through which payment has gone through pub connector: Connector, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, + /// status provided by the router pub status: String, + /// optional error provided by the router pub router_error: Option, + /// decision to be taken for auto retries flow pub decision: GsmDecision, + /// indicates if step_up retry is possible pub step_up_possible: bool, + /// error code unified across the connectors pub unified_code: Option, + /// error message unified across the connectors pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmRetrieveRequest { + /// The connector through which payment has gone through pub connector: Connector, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, } @@ -50,48 +66,79 @@ pub enum GsmDecision { #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmUpdateRequest { + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, + /// status provided by the router pub status: Option, + /// optional error provided by the router pub router_error: Option, + /// decision to be taken for auto retries flow pub decision: Option, + /// indicates if step_up retry is possible pub step_up_possible: Option, + /// error code unified across the connectors pub unified_code: Option, + /// error message unified across the connectors pub unified_message: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GsmDeleteRequest { + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, } #[derive(Debug, serde::Serialize, ToSchema)] pub struct GsmDeleteResponse { pub gsm_rule_delete: bool, + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, } #[derive(serde::Serialize, Debug, ToSchema)] pub struct GsmResponse { + /// The connector through which payment has gone through pub connector: String, + /// The flow in which the code and message occurred for a connector pub flow: String, + /// The sub_flow in which the code and message occurred for a connector pub sub_flow: String, + /// code received from the connector pub code: String, + /// message received from the connector pub message: String, + /// status provided by the router pub status: String, + /// optional error provided by the router pub router_error: Option, + /// decision to be taken for auto retries flow pub decision: String, + /// indicates if step_up retry is possible pub step_up_possible: bool, + /// error code unified across the connectors pub unified_code: Option, + /// error message unified across the connectors pub unified_message: Option, } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 984e6dbffff9..173272e25d80 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -21,7 +21,7 @@ use crate::{ #[serde(deny_unknown_fields)] pub struct PaymentMethodCreate { /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethodType,example = "card")] + #[schema(value_type = PaymentMethod,example = "card")] pub payment_method: api_enums::PaymentMethod, /// This is a sub-category of payment method. @@ -139,7 +139,7 @@ pub struct PaymentMethodResponse { pub payment_method_id: String, /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethodType, example = "card")] + #[schema(value_type = PaymentMethod, example = "card")] pub payment_method: api_enums::PaymentMethod, /// This is a sub-category of payment method. @@ -727,7 +727,7 @@ pub struct CustomerPaymentMethod { pub customer_id: String, /// The type of payment method use for the payment. - #[schema(value_type = PaymentMethodType,example = "card")] + #[schema(value_type = PaymentMethod,example = "card")] pub payment_method: api_enums::PaymentMethod, /// This is a sub-category of payment method. diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 24ac72175d71..57c66dc5a7b6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -82,9 +82,25 @@ pub struct CustomerDetails { ToSchema, router_derive::PolymorphicSchema, )] -#[generate_schemas(PaymentsCreateRequest)] +#[generate_schemas(PaymentsCreateRequest, PaymentsUpdateRequest, PaymentsConfirmRequest)] #[serde(deny_unknown_fields)] pub struct PaymentsRequest { + /// The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency + #[schema(value_type = Option, example = 6540)] + #[serde(default, deserialize_with = "amount::deserialize_option")] + #[mandatory_in(PaymentsCreateRequest = u64)] + // Makes the field mandatory in PaymentsCreateRequest + pub amount: Option, + + /// The three letter ISO currency code in uppercase. Eg: 'USD' to charge US Dollars + #[schema(example = "USD", value_type = Option)] + #[mandatory_in(PaymentsCreateRequest = Currency)] + pub currency: Option, + + /// The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount. + #[schema(example = 6540)] + pub amount_to_capture: Option, + /// Unique identifier for the payment. This ensures idempotency for multiple payments /// that have been done by a single merchant. This field is auto generated and is returned in the API response. #[schema( @@ -101,36 +117,26 @@ pub struct PaymentsRequest { #[schema(max_length = 255, example = "merchant_1668273825")] pub merchant_id: Option, - /// The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., - #[schema(value_type = Option, example = 6540)] - #[serde(default, deserialize_with = "amount::deserialize_option")] - #[mandatory_in(PaymentsCreateRequest)] - // Makes the field mandatory in PaymentsCreateRequest - pub amount: Option, - - #[schema(value_type = Option, example = json!({ + #[schema(value_type = Option, example = json!({ "type": "single", - "data": "stripe" + "data": {"connector": "stripe", "merchant_connector_id": "mca_123"} }))] pub routing: Option, - /// This allows the merchant to manually select a connector with which the payment can go through + /// This allows to manually select a connector with which the payment can go through #[schema(value_type = Option>, max_length = 255, example = json!(["stripe", "adyen"]))] pub connector: Option>, - /// The currency of the payment request can be specified here - #[schema(value_type = Option, example = "USD")] - #[mandatory_in(PaymentsCreateRequest)] - pub currency: Option, - - /// This is the instruction for capture/ debit the money from the users' card. On the other hand authorization refers to blocking the amount on the users' payment method. + /// Default value if not passed is set to 'automatic' which results in Auth and Capture in one single API request. Pass 'manual' or 'manual_multiple' in case you want do a separate Auth and Capture by first authorizing and placing a hold on your customer's funds so that you can use the Payments/Capture endpoint later to capture the authorized amount. Pass 'manual' if you want to only capture the amount later once or 'manual_multiple' if you want to capture the funds multiple times later. Both 'manual' and 'manual_multiple' are only supported by a specific list of processors #[schema(value_type = Option, example = "automatic")] pub capture_method: Option, - /// The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., - /// If not provided, the default amount_to_capture will be the payment amount. - #[schema(example = 6540)] - pub amount_to_capture: Option, + /// Pass this parameter to force 3DS or non 3DS auth for this payment. Some connectors will still force 3DS auth even in case of passing 'no_three_ds' here and vice versa. Default value is 'no_three_ds' if not set + #[schema(value_type = Option, example = "no_three_ds", default = "three_ds")] + pub authentication_type: Option, + + /// The billing details of the customer + pub billing: Option
, /// A timestamp (ISO 8601 code) that determines when the payment should be captured. /// Providing this field will automatically set `capture` to true @@ -142,23 +148,19 @@ pub struct PaymentsRequest { #[schema(default = false, example = true)] pub confirm: Option, - /// The details of a customer for this payment - /// This will create the customer if `customer.id` does not exist - /// If customer id already exists, it will update the details of the customer + /// Passing this object creates a new customer or attaches an existing customer to the payment pub customer: Option, - /// The identifier for the customer object. - /// This field will be deprecated soon, use the customer object instead + /// The identifier for the customer object. This field will be deprecated soon, use the customer object instead #[schema(max_length = 255, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, - /// The customer's email address - /// This field will be deprecated soon, use the customer object instead + /// The customer's email address This field will be deprecated soon, use the customer object instead #[schema(max_length = 255, value_type = Option, example = "johntest@test.com")] pub email: Option, - /// description: The customer's name - /// This field will be deprecated soon, use the customer object instead + /// The customer's name. + /// This field will be deprecated soon, use the customer object instead. #[schema(value_type = Option, max_length = 255, example = "John Test")] pub name: Option>, @@ -172,11 +174,11 @@ pub struct PaymentsRequest { #[schema(max_length = 255, example = "+1")] pub phone_country_code: Option, - /// Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`. + /// Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory #[schema(example = true)] pub off_session: Option, - /// A description of the payment + /// A description for the payment #[schema(example = "It's my first payment request")] pub description: Option, @@ -187,10 +189,6 @@ pub struct PaymentsRequest { #[schema(value_type = Option, example = "off_session")] pub setup_future_usage: Option, - /// The transaction authentication can be set to undergo payer authentication. - #[schema(value_type = Option, example = "no_three_ds", default = "three_ds")] - pub authentication_type: Option, - /// The payment method information provided for making a payment #[schema(example = "bank_transfer")] pub payment_method_data: Option, @@ -203,17 +201,13 @@ pub struct PaymentsRequest { #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] pub payment_token: Option, - /// This is used when payment is to be confirmed and the card is not saved. - /// This field will be deprecated soon, use the CardToken object instead + /// This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead #[schema(value_type = Option, deprecated)] pub card_cvc: Option>, /// The shipping address for the payment pub shipping: Option
, - /// The billing address for the payment - pub billing: Option
, - /// For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters. #[schema(max_length = 255, example = "Hyperswitch Router")] pub statement_descriptor_name: Option, @@ -222,28 +216,30 @@ pub struct PaymentsRequest { #[schema(max_length = 255, example = "Payment for shoes purchase")] pub statement_descriptor_suffix: Option, - /// Information about the product , quantity and amount for connectors. (e.g. Klarna) + /// Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount #[schema(value_type = Option>, example = r#"[{ - "product_name": "gillete creme", - "quantity": 15, - "amount" : 900 + "product_name": "Apple iPhone 16", + "quantity": 1, + "amount" : 69000 "product_img_link" : "https://dummy-img-link.com" }]"#)] pub order_details: Option>, /// It's a token used for client side verification. #[schema(example = "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo")] + #[remove_in(PaymentsUpdateRequest, PaymentsCreateRequest)] pub client_secret: Option, - /// Provide mandate information for creating a mandate + /// Passing this object during payments creates a mandate. The mandate_type sub object is passed by the server usually and the customer_acceptance sub object is usually passed by the SDK or client pub mandate_data: Option, - /// A unique identifier to link the payment to a mandate, can be use instead of payment_method_data + /// A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data #[schema(max_length = 255, example = "mandate_iwer89rnjef349dni3")] + #[remove_in(PaymentsUpdateRequest)] pub mandate_id: Option, /// Additional details required by 3DS 2.0 - #[schema(value_type = Option, example = r#"{ + #[schema(value_type = Option, example = r#"{ "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "language": "nl-NL", @@ -256,7 +252,7 @@ pub struct PaymentsRequest { }"#)] pub browser_info: Option, - /// Payment Experience for the current payment + /// To indicate the type of payment experience that the payment method would go through #[schema(value_type = Option, example = "redirect_to_url")] pub payment_experience: Option, @@ -264,23 +260,28 @@ pub struct PaymentsRequest { #[schema(value_type = Option, example = "google_pay")] pub payment_method_type: Option, - /// Business country of the merchant for this payment + /// Business country of the merchant for this payment. + /// To be deprecated soon. Pass the profile_id instead #[schema(value_type = Option, example = "US")] + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] pub business_country: Option, - /// Business label of the merchant for this payment + /// Business label of the merchant for this payment. + /// To be deprecated soon. Pass the profile_id instead #[schema(example = "food")] + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] pub business_label: Option, /// Merchant connector details used to make payments. #[schema(value_type = Option)] pub merchant_connector_details: Option, - /// Allowed Payment Method Types for a given PaymentIntent + /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent #[schema(value_type = Option>)] pub allowed_payment_method_types: Option>, /// Business sub label for the payment + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest, PaymentsCreateRequest)] pub business_sub_label: Option, /// Denotes the retry action @@ -307,9 +308,11 @@ pub struct PaymentsRequest { /// The business profile to use for this payment, if not passed the default business profile /// associated with the merchant account will be used. + #[remove_in(PaymentsUpdateRequest, PaymentsConfirmRequest)] pub profile_id: Option, /// surcharge_details for this payment + #[remove_in(PaymentsConfirmRequest)] #[schema(value_type = Option)] pub surcharge_details: Option, @@ -347,6 +350,44 @@ pub struct RequestSurchargeDetails { pub tax_amount: Option, } +/// Browser information to be used for 3DS 2.0 +#[derive(ToSchema)] +pub struct BrowserInformation { + /// Color depth supported by the browser + pub color_depth: Option, + + /// Whether java is enabled in the browser + pub java_enabled: Option, + + /// Whether javascript is enabled in the browser + pub java_script_enabled: Option, + + /// Language supported + pub language: Option, + + /// The screen height in pixels + pub screen_height: Option, + + /// The screen width in pixels + pub screen_width: Option, + + /// Time zone of the client + pub time_zone: Option, + + /// Ip address of the client + #[schema(value_type = Option)] + pub ip_address: Option, + + /// List of headers that are accepted + #[schema( + example = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + )] + pub accept_header: Option, + + /// User-agent of the browser + pub user_agent: Option, +} + impl RequestSurchargeDetails { pub fn is_surcharge_zero(&self) -> bool { self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 @@ -662,6 +703,7 @@ pub struct CustomerAcceptance { #[derive(Default, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq, Clone, ToSchema)] #[serde(rename_all = "lowercase")] +/// This is used to indicate if the mandate was accepted online or offline pub enum AcceptanceType { Online, #[default] @@ -876,19 +918,33 @@ pub enum BankDebitData { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum PaymentMethodData { + #[schema(title = "Card")] Card(Card), + #[schema(title = "CardRedirect")] CardRedirect(CardRedirectData), + #[schema(title = "Wallet")] Wallet(WalletData), + #[schema(title = "PayLater")] PayLater(PayLaterData), + #[schema(title = "BankRedirect")] BankRedirect(BankRedirectData), + #[schema(title = "BankDebit")] BankDebit(BankDebitData), + #[schema(title = "BankTransfer")] BankTransfer(Box), + #[schema(title = "Crypto")] Crypto(CryptoData), + #[schema(title = "MandatePayment")] MandatePayment, + #[schema(title = "Reward")] Reward, + #[schema(title = "Upi")] Upi(UpiData), + #[schema(title = "Voucher")] Voucher(VoucherData), + #[schema(title = "GiftCard")] GiftCard(Box), + #[schema(title = "CardToken")] CardToken(CardToken), } @@ -1788,6 +1844,7 @@ pub struct Address { } // used by customers also, could be moved outside +/// Address details #[derive(Clone, Default, Debug, Eq, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] #[serde(deny_unknown_fields)] pub struct AddressDetails { @@ -3132,7 +3189,7 @@ pub struct PaymentsCancelRequest { /// The reason for the payment cancel pub cancellation_reason: Option, /// Merchant connector details used to make payments. - #[schema(value_type = MerchantConnectorDetailsWrap)] + #[schema(value_type = Option)] pub merchant_connector_details: Option, } diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 1a0668023f02..97182df0a5e5 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -9,21 +9,21 @@ use crate::{admin, enums}; #[derive(Default, Debug, ToSchema, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct RefundRequest { - /// Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refund initiated against the same payment. If the identifiers is not defined by the merchant, this filed shall be auto generated and provide in the API response. It is recommended to generate uuid(v4) as the refund_id. + /// The payment id against which refund is to be intiated #[schema( max_length = 30, min_length = 30, - example = "ref_mbabizu24mvu3mela5njyhpit4" + example = "pay_mbabizu24mvu3mela5njyhpit4" )] - pub refund_id: Option, + pub payment_id: String, - /// Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc. If not provided, this will default to the full payment amount + /// Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refunds initiated against the same payment. If this is not passed by the merchant, this field shall be auto generated and provided in the API response. It is recommended to generate uuid(v4) as the refund_id. #[schema( max_length = 30, min_length = 30, - example = "pay_mbabizu24mvu3mela5njyhpit4" + example = "ref_mbabizu24mvu3mela5njyhpit4" )] - pub payment_id: String, + pub refund_id: Option, /// The identifier for the Merchant Account #[schema(max_length = 255, example = "y3oqhf46pyzuxjbcn2giaqnb44")] @@ -33,11 +33,11 @@ pub struct RefundRequest { #[schema(minimum = 100, example = 6540)] pub amount: Option, - /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive + /// Reason for the refund. Often useful for displaying to users and your customer support executive. In case the payment went through Stripe, this field needs to be passed with one of these enums: `duplicate`, `fraudulent`, or `requested_by_customer` #[schema(max_length = 255, example = "Customer returned the product")] pub reason: Option, - /// The type of refund based on waiting time for processing: Scheduled or Instant Refund + /// To indicate whether to refund needs to be instant or scheduled. Default value is instant #[schema(default = "Instant", example = "Instant")] pub refund_type: Option, @@ -87,6 +87,7 @@ pub struct RefundUpdateRequest { pub metadata: Option, } +/// To indicate whether to refund needs to be instant or scheduled #[derive( Default, Debug, Clone, Copy, ToSchema, Deserialize, Serialize, Eq, PartialEq, strum::Display, )] @@ -99,18 +100,18 @@ pub enum RefundType { #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ToSchema)] pub struct RefundResponse { - /// The identifier for refund + /// Unique Identifier for the refund pub refund_id: String, - /// The identifier for payment + /// The payment id against which refund is intiated pub payment_id: String, /// The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc pub amount: i64, /// The three-letter ISO currency code pub currency: String, - /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive - pub reason: Option, /// The status for refund pub status: RefundStatus, + /// An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive + pub reason: Option, /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object #[schema(value_type = Option)] pub metadata: Option, diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 2236714da1d1..2775034c88c0 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use common_utils::errors::ParsingError; use error_stack::IntoReport; -use euclid::{ +pub use euclid::{ dssa::types::EuclidAnalysable, frontend::{ ast, @@ -10,10 +10,11 @@ use euclid::{ }, }; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::enums::{self, RoutableConnectors}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum ConnectorSelection { Priority(Vec), @@ -31,7 +32,7 @@ impl ConnectorSelection { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RoutingConfigRequest { pub name: Option, pub description: Option, @@ -39,7 +40,7 @@ pub struct RoutingConfigRequest { pub profile_id: Option, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, ToSchema)] pub struct ProfileDefaultRoutingConfig { pub profile_id: String, pub connectors: Vec, @@ -60,19 +61,21 @@ pub struct RoutingRetrieveLinkQuery { pub profile_id: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +/// Response of the retrieved routing configs for a merchant account pub struct RoutingRetrieveResponse { pub algorithm: Option, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, ToSchema)] #[serde(untagged)] pub enum LinkedRoutingConfigRetrieveResponse { MerchantAccountBased(RoutingRetrieveResponse), ProfileBased(Vec), } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +/// Routing Algorithm specific to merchants pub struct MerchantRoutingAlgorithm { pub id: String, #[cfg(feature = "business_profile_routing")] @@ -153,14 +156,14 @@ impl EuclidAnalysable for ConnectorSelection { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct ConnectorVolumeSplit { pub connector: RoutableConnectorChoice, pub split: u8, } #[cfg(feature = "connector_choice_bcompat")] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] pub enum RoutableChoiceKind { OnlyConnector, FullStruct, @@ -180,15 +183,18 @@ pub enum RoutableChoiceSerde { }, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[cfg_attr( feature = "connector_choice_bcompat", serde(from = "RoutableChoiceSerde"), serde(into = "RoutableChoiceSerde") )] #[cfg_attr(not(feature = "connector_choice_bcompat"), derive(PartialEq, Eq))] + +/// Routable Connector chosen for a payment pub struct RoutableConnectorChoice { #[cfg(feature = "connector_choice_bcompat")] + #[serde(skip)] pub choice_kind: RoutableChoiceKind, pub connector: RoutableConnectors, #[cfg(feature = "connector_choice_mca_id")] @@ -322,7 +328,7 @@ impl DetailedConnectorChoice { } } -#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display)] +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, strum::Display, ToSchema)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutingAlgorithmKind { @@ -339,17 +345,19 @@ pub struct RoutingPayloadWrapper { pub profile_id: String, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[serde( tag = "type", content = "data", rename_all = "snake_case", try_from = "RoutingAlgorithmSerde" )] +/// Routing Algorithm kind pub enum RoutingAlgorithm { Single(Box), Priority(Vec), VolumeSplit(Vec), + #[schema(value_type=ProgramConnectorSelection)] Advanced(euclid::frontend::ast::Program), } @@ -390,7 +398,7 @@ impl TryFrom for RoutingAlgorithm { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[serde( tag = "type", content = "data", @@ -399,8 +407,11 @@ impl TryFrom for RoutingAlgorithm { into = "StraightThroughAlgorithmSerde" )] pub enum StraightThroughAlgorithm { + #[schema(title = "Single")] Single(Box), + #[schema(title = "Priority")] Priority(Vec), + #[schema(title = "VolumeSplit")] VolumeSplit(Vec), } @@ -516,7 +527,7 @@ impl RoutingAlgorithmRef { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RoutingDictionaryRecord { pub id: String, @@ -529,14 +540,14 @@ pub struct RoutingDictionaryRecord { pub modified_at: i64, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RoutingDictionary { pub merchant_id: String, pub active_id: Option, pub records: Vec, } -#[derive(serde::Serialize, serde::Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Debug, ToSchema)] #[serde(untagged)] pub enum RoutingKind { Config(RoutingDictionary), diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 7b3564732bf9..37aaac42e0df 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -169,13 +169,13 @@ pub struct OutgoingWebhook { #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(tag = "type", content = "object", rename_all = "snake_case")] pub enum OutgoingWebhookContent { - #[schema(value_type = PaymentsResponse)] + #[schema(value_type = PaymentsResponse, title = "PaymentsResponse")] PaymentDetails(payments::PaymentsResponse), - #[schema(value_type = RefundResponse)] + #[schema(value_type = RefundResponse, title = "RefundResponse")] RefundDetails(refunds::RefundResponse), - #[schema(value_type = DisputeResponse)] + #[schema(value_type = DisputeResponse, title = "DisputeResponse")] DisputeDetails(Box), - #[schema(value_type = MandateResponse)] + #[schema(value_type = MandateResponse, title = "MandateResponse")] MandateDetails(Box), } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index 3ed01ca2a97a..f82d8e7a825b 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true [features] dummy_connector = [] +openapi = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 949cc2e0034d..c0a363042ec3 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -74,10 +74,12 @@ pub enum AttemptStatus { strum::EnumString, strum::EnumIter, strum::EnumVariantNames, + ToSchema, )] #[router_derive::diesel_enum(storage_type = "db_enum")] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] +/// Connectors eligible for payments routing pub enum RoutableConnectors { #[cfg(feature = "dummy_connector")] #[serde(rename = "phonypay")] @@ -328,6 +330,7 @@ pub enum CaptureMethod { Scheduled, } +/// Type of the Connector for the financial use case. Could range from Payments to Accounting to Banking. #[derive( Clone, Copy, @@ -364,6 +367,7 @@ pub enum ConnectorType { PaymentMethodAuth, } +/// The three letter ISO currency code in uppercase. Eg: 'USD' for the United States Dollar. #[allow(clippy::upper_case_acronyms)] #[derive( Clone, @@ -1074,6 +1078,7 @@ pub enum PaymentMethodIssuerCode { JpBacs, } +/// To indicate the type of payment experience that the customer would go through #[derive( Eq, strum::EnumString, @@ -1109,6 +1114,7 @@ pub enum PaymentExperience { DisplayWaitScreen, } +/// Indicates the sub type of payment method. Eg: 'google_pay' & 'apple_pay' for wallets. #[derive( Clone, Copy, @@ -1214,6 +1220,7 @@ pub enum PaymentMethodType { PayEasy, } +/// Indicates the type of payment method. Eg: 'card', 'wallet', etc. #[derive( Clone, Copy, @@ -1249,6 +1256,7 @@ pub enum PaymentMethod { GiftCard, } +/// To be used to specify the type of payment. Use 'setup_mandate' in case of zero auth flow. #[derive( Clone, Copy, @@ -1297,7 +1305,7 @@ pub enum RefundStatus { TransactionFailure, } -/// The status of the mandate, which indicates whether it can be used to initiate a payment +/// The status of the mandate, which indicates whether it can be used to initiate a payment. #[derive( Clone, Copy, @@ -1322,6 +1330,7 @@ pub enum MandateStatus { Revoked, } +/// Indicates the card network. #[derive( Clone, Debug, diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs index 9d41569384f1..c07b2112db2c 100644 --- a/crates/common_utils/src/macros.rs +++ b/crates/common_utils/src/macros.rs @@ -54,6 +54,18 @@ macro_rules! async_spawn { }; } +/// Use this to ensure that the corresponding +/// openapi route has been implemented in the openapi crate +#[macro_export] +macro_rules! openapi_route { + ($route_name: ident) => {{ + #[cfg(feature = "openapi")] + use openapi::routes::$route_name as _; + + $route_name + }}; +} + #[macro_export] macro_rules! fallback_reverse_lookup_not_found { ($a:expr,$b:expr) => { diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml index 08b9f0af28b7..415398105aef 100644 --- a/crates/euclid/Cargo.toml +++ b/crates/euclid/Cargo.toml @@ -16,6 +16,7 @@ serde = { version = "1.0.193", features = ["derive", "rc"] } serde_json = "1.0.108" strum = { version = "0.25", features = ["derive"] } thiserror = "1.0.43" +utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party dependencies common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/euclid/src/frontend/ast.rs b/crates/euclid/src/frontend/ast.rs index 0dad9b53c323..905bf04de0af 100644 --- a/crates/euclid/src/frontend/ast.rs +++ b/crates/euclid/src/frontend/ast.rs @@ -4,6 +4,7 @@ pub mod parser; use common_enums::RoutableConnectors; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::types::{DataType, Metadata}; @@ -14,14 +15,14 @@ pub struct ConnectorChoice { pub sub_label: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] pub struct MetadataValue { pub key: String, pub value: String, } /// Represents a value in the DSL -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "value", rename_all = "snake_case")] pub enum ValueType { /// Represents a number literal @@ -60,7 +61,7 @@ impl ValueType { } /// Represents a number comparison for "NumberComparisonArrayValue" -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct NumberComparison { pub comparison_type: ComparisonType, @@ -68,7 +69,7 @@ pub struct NumberComparison { } /// Conditional comparison type -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ComparisonType { Equal, @@ -80,7 +81,7 @@ pub enum ComparisonType { } /// Represents a single comparison condition. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Comparison { /// The left hand side which will always be a domain input identifier like "payment.method.cardtype" @@ -92,6 +93,7 @@ pub struct Comparison { /// Additional metadata that the Static Analyzer and Backend does not touch. /// This can be used to store useful information for the frontend and is required for communication /// between the static analyzer and the frontend. + #[schema(value_type=HashMap)] pub metadata: Metadata, } @@ -112,9 +114,10 @@ pub type IfCondition = Vec; /// } /// } /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IfStatement { + #[schema(value_type=Vec)] pub condition: IfCondition, pub nested: Option>, } @@ -134,8 +137,9 @@ pub struct IfStatement { /// } /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[aliases(RuleConnectorSelection = Rule)] pub struct Rule { pub name: String, #[serde(alias = "routingOutput")] @@ -145,10 +149,43 @@ pub struct Rule { /// The program, having a default connector selection and /// a bunch of rules. Also can hold arbitrary metadata. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[aliases(ProgramConnectorSelection = Program)] pub struct Program { pub default_selection: O, + #[schema(value_type=RuleConnectorSelection)] pub rules: Vec>, + #[schema(value_type=HashMap)] pub metadata: Metadata, } + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RoutableConnectorChoice { + #[cfg(feature = "connector_choice_bcompat")] + #[serde(skip)] + pub choice_kind: RoutableChoiceKind, + #[cfg(feature = "connector_choice_mca_id")] + pub merchant_connector_id: Option, + #[cfg(not(feature = "connector_choice_mca_id"))] + pub sub_label: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub enum RoutableChoiceKind { + OnlyConnector, + FullStruct, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ConnectorVolumeSplit { + pub connector: RoutableConnectorChoice, + pub split: u8, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum ConnectorSelection { + Priority(Vec), + VolumeSplit(Vec), +} diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml new file mode 100644 index 000000000000..236916862da3 --- /dev/null +++ b/crates/openapi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "openapi" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } +serde_json = "1.0.96" + +api_models = { version = "0.1.0", path = "../api_models", features = ["openapi"] } diff --git a/crates/openapi/src/lib.rs b/crates/openapi/src/lib.rs new file mode 100644 index 000000000000..27d67445bc2d --- /dev/null +++ b/crates/openapi/src/lib.rs @@ -0,0 +1,2 @@ +mod openapi; +pub mod routes; diff --git a/crates/openapi/src/main.rs b/crates/openapi/src/main.rs new file mode 100644 index 000000000000..88bcb8896b42 --- /dev/null +++ b/crates/openapi/src/main.rs @@ -0,0 +1,15 @@ +mod openapi; +mod routes; + +fn main() { + let file_path = "openapi/openapi_spec.json"; + #[allow(clippy::expect_used)] + std::fs::write( + file_path, + ::openapi() + .to_pretty_json() + .expect("Failed to serialize OpenAPI specification as JSON"), + ) + .expect("Failed to write OpenAPI specification to file"); + println!("Successfully saved OpenAPI specification file at '{file_path}'"); +} diff --git a/crates/router/src/openapi.rs b/crates/openapi/src/openapi.rs similarity index 65% rename from crates/router/src/openapi.rs rename to crates/openapi/src/openapi.rs index 174926c7d360..3d4a1893ad6d 100644 --- a/crates/router/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -1,4 +1,5 @@ -#[cfg(feature = "openapi")] +use crate::routes; + #[derive(utoipa::OpenApi)] #[openapi( info( @@ -59,97 +60,138 @@ Never share your secret api keys. Keep them guarded and secure. (name = "Customers", description = "Create and manage customers"), (name = "Payment Methods", description = "Create and manage payment methods of customers"), (name = "Disputes", description = "Manage disputes"), - // (name = "API Key", description = "Create and manage API Keys"), + (name = "API Key", description = "Create and manage API Keys"), (name = "Payouts", description = "Create and manage payouts"), (name = "payment link", description = "Create payment link"), + (name = "Routing", description = "Create and manage routing configurations"), ), + // The paths will be displayed in the same order as they are registered here paths( - crate::routes::refunds::refunds_create, - crate::routes::refunds::refunds_retrieve, - crate::routes::refunds::refunds_update, - crate::routes::refunds::refunds_list, - // Commenting this out as these are admin apis and not to be used by the merchant - // crate::routes::admin::merchant_account_create, - // crate::routes::admin::retrieve_merchant_account, - // crate::routes::admin::update_merchant_account, - // crate::routes::admin::delete_merchant_account, - crate::routes::admin::payment_connector_create, - crate::routes::admin::payment_connector_retrieve, - crate::routes::admin::payment_connector_list, - crate::routes::admin::payment_connector_update, - crate::routes::admin::payment_connector_delete, - crate::routes::mandates::get_mandate, - crate::routes::mandates::revoke_mandate, - crate::routes::payments::payments_create, - // crate::routes::payments::payments_start, - crate::routes::payments::payments_retrieve, - crate::routes::payments::payments_update, - crate::routes::payments::payments_confirm, - crate::routes::payments::payments_capture, - crate::routes::payments::payments_connector_session, - // crate::routes::payments::payments_redirect_response, - crate::routes::payments::payments_cancel, - crate::routes::payments::payments_list, - crate::routes::payment_methods::create_payment_method_api, - crate::routes::payment_methods::list_payment_method_api, - crate::routes::payment_methods::list_customer_payment_method_api, - crate::routes::payment_methods::list_customer_payment_method_api_client, - crate::routes::payment_methods::payment_method_retrieve_api, - crate::routes::payment_methods::payment_method_update_api, - crate::routes::payment_methods::payment_method_delete_api, - crate::routes::customers::customers_create, - crate::routes::customers::customers_retrieve, - crate::routes::customers::customers_update, - crate::routes::customers::customers_delete, - crate::routes::customers::customers_list, - // crate::routes::api_keys::api_key_create, - // crate::routes::api_keys::api_key_retrieve, - // crate::routes::api_keys::api_key_update, - // crate::routes::api_keys::api_key_revoke, - // crate::routes::api_keys::api_key_list, - crate::routes::disputes::retrieve_disputes_list, - crate::routes::disputes::retrieve_dispute, - crate::routes::payouts::payouts_create, - crate::routes::payouts::payouts_cancel, - crate::routes::payouts::payouts_fulfill, - crate::routes::payouts::payouts_retrieve, - crate::routes::payouts::payouts_update, - crate::routes::payment_link::payment_link_retrieve, - crate::routes::gsm::create_gsm_rule, - crate::routes::gsm::get_gsm_rule, - crate::routes::gsm::update_gsm_rule, - crate::routes::gsm::delete_gsm_rule, - crate::routes::blocklist::add_entry_to_blocklist, - crate::routes::blocklist::list_blocked_payment_methods, - crate::routes::blocklist::remove_entry_from_blocklist + // Routes for payments + routes::payments::payments_create, + routes::payments::payments_update, + routes::payments::payments_confirm, + routes::payments::payments_retrieve, + routes::payments::payments_capture, + routes::payments::payments_connector_session, + routes::payments::payments_cancel, + routes::payments::payments_list, + routes::payments::payments_incremental_authorization, + routes::payment_link::payment_link_retrieve, + + // Routes for refunds + routes::refunds::refunds_create, + routes::refunds::refunds_retrieve, + routes::refunds::refunds_update, + routes::refunds::refunds_list, + + // Routes for merchant account + routes::merchant_account::merchant_account_create, + routes::merchant_account::retrieve_merchant_account, + routes::merchant_account::update_merchant_account, + routes::merchant_account::delete_merchant_account, + + // Routes for merchant connector account + routes::merchant_connector_account::payment_connector_create, + routes::merchant_connector_account::payment_connector_retrieve, + routes::merchant_connector_account::payment_connector_list, + routes::merchant_connector_account::payment_connector_update, + routes::merchant_connector_account::payment_connector_delete, + + //Routes for gsm + routes::gsm::create_gsm_rule, + routes::gsm::get_gsm_rule, + routes::gsm::update_gsm_rule, + routes::gsm::delete_gsm_rule, + + // Routes for mandates + routes::mandates::get_mandate, + routes::mandates::revoke_mandate, + + //Routes for customers + routes::customers::customers_create, + routes::customers::customers_retrieve, + routes::customers::customers_list, + routes::customers::customers_update, + routes::customers::customers_delete, + + //Routes for payment methods + routes::payment_method::create_payment_method_api, + routes::payment_method::list_payment_method_api, + routes::payment_method::list_customer_payment_method_api, + routes::payment_method::list_customer_payment_method_api_client, + routes::payment_method::payment_method_retrieve_api, + routes::payment_method::payment_method_update_api, + routes::payment_method::payment_method_delete_api, + + // Routes for Business Profile + routes::business_profile::business_profile_create, + routes::business_profile::business_profiles_list, + routes::business_profile::business_profiles_update, + routes::business_profile::business_profiles_delete, + routes::business_profile::business_profiles_retrieve, + + // Routes for disputes + routes::disputes::retrieve_dispute, + routes::disputes::retrieve_disputes_list, + + // Routes for routing + routes::routing::routing_create_config, + routes::routing::routing_link_config, + routes::routing::routing_retrieve_config, + routes::routing::list_routing_configs, + routes::routing::routing_unlink_config, + routes::routing::routing_update_default_config, + routes::routing::routing_retrieve_default_config, + routes::routing::routing_retrieve_linked_config, + routes::routing::routing_retrieve_default_config_for_profiles, + routes::routing::routing_update_default_config_for_profile, + + // Routes for blocklist + routes::blocklist::remove_entry_from_blocklist, + routes::blocklist::list_blocked_payment_methods, + routes::blocklist::add_entry_to_blocklist, + + // Routes for payouts + routes::payouts::payouts_create, + routes::payouts::payouts_retrieve, + routes::payouts::payouts_update, + routes::payouts::payouts_cancel, + routes::payouts::payouts_fulfill, + + // Routes for api keys + routes::api_keys::api_key_create, + routes::api_keys::api_key_retrieve, + routes::api_keys::api_key_update, + routes::api_keys::api_key_revoke ), components(schemas( - crate::types::api::refunds::RefundRequest, - crate::types::api::refunds::RefundType, - crate::types::api::refunds::RefundResponse, - crate::types::api::refunds::RefundStatus, - crate::types::api::refunds::RefundUpdateRequest, - crate::types::api::admin::MerchantAccountCreate, - crate::types::api::admin::MerchantAccountUpdate, - crate::types::api::admin::MerchantAccountDeleteResponse, - crate::types::api::admin::MerchantConnectorDeleteResponse, - crate::types::api::admin::MerchantConnectorResponse, - crate::types::api::customers::CustomerRequest, - crate::types::api::customers::CustomerDeleteResponse, - crate::types::api::payment_methods::PaymentMethodCreate, - crate::types::api::payment_methods::PaymentMethodResponse, - crate::types::api::payment_methods::PaymentMethodList, - crate::types::api::payment_methods::CustomerPaymentMethod, - crate::types::api::payment_methods::PaymentMethodListResponse, - crate::types::api::payment_methods::CustomerPaymentMethodsListResponse, - crate::types::api::payment_methods::PaymentMethodDeleteResponse, - crate::types::api::payment_methods::PaymentMethodUpdate, - crate::types::api::payment_methods::CardDetailFromLocker, - crate::types::api::payment_methods::CardDetail, + api_models::refunds::RefundRequest, + api_models::refunds::RefundType, + api_models::refunds::RefundResponse, + api_models::refunds::RefundStatus, + api_models::refunds::RefundUpdateRequest, + api_models::admin::MerchantAccountCreate, + api_models::admin::MerchantAccountUpdate, + api_models::admin::MerchantAccountDeleteResponse, + api_models::admin::MerchantConnectorDeleteResponse, + api_models::admin::MerchantConnectorResponse, + api_models::customers::CustomerRequest, + api_models::customers::CustomerDeleteResponse, + api_models::payment_methods::PaymentMethodCreate, + api_models::payment_methods::PaymentMethodResponse, + api_models::payment_methods::PaymentMethodList, + api_models::payment_methods::CustomerPaymentMethod, + api_models::payment_methods::PaymentMethodListResponse, + api_models::payment_methods::CustomerPaymentMethodsListResponse, + api_models::payment_methods::PaymentMethodDeleteResponse, + api_models::payment_methods::PaymentMethodUpdate, + api_models::payment_methods::CardDetailFromLocker, + api_models::payment_methods::CardDetail, + api_models::payment_methods::RequestPaymentMethodTypes, api_models::customers::CustomerResponse, api_models::admin::AcceptedCountries, api_models::admin::AcceptedCurrencies, - api_models::enums::RoutingAlgorithm, api_models::enums::PaymentType, api_models::enums::PaymentMethod, api_models::enums::PaymentMethodType, @@ -189,6 +231,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::MerchantConnectorDetailsWrap, api_models::admin::MerchantConnectorDetails, api_models::admin::MerchantConnectorWebhookDetails, + api_models::admin::BusinessProfileCreate, + api_models::admin::BusinessProfileResponse, api_models::admin::BusinessPaymentLinkConfig, api_models::admin::PaymentLinkConfigRequest, api_models::admin::PaymentLinkConfig, @@ -257,6 +301,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::CustomerAcceptance, api_models::payments::PaymentsRequest, api_models::payments::PaymentsCreateRequest, + api_models::payments::PaymentsUpdateRequest, + api_models::payments::PaymentsConfirmRequest, api_models::payments::PaymentsResponse, api_models::payments::PaymentsStartRequest, api_models::payments::PaymentRetrieveBody, @@ -321,14 +367,15 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::RequestSurchargeDetails, api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, + api_models::payments::PaymentsIncrementalAuthorizationRequest, api_models::payments::IncrementalAuthorizationResponse, + api_models::payments::BrowserInformation, api_models::payments::PaymentCreatePaymentLinkConfig, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, api_models::payment_methods::SurchargeDetailsResponse, api_models::payment_methods::SurchargeResponse, api_models::payment_methods::SurchargePercentage, - api_models::payment_methods::RequestPaymentMethodTypes, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, @@ -359,25 +406,50 @@ Never share your secret api keys. Keep them guarded and secure. api_models::webhooks::OutgoingWebhook, api_models::webhooks::OutgoingWebhookContent, api_models::enums::EventType, - crate::types::api::admin::MerchantAccountResponse, - crate::types::api::admin::MerchantConnectorId, - crate::types::api::admin::MerchantDetails, - crate::types::api::admin::WebhookDetails, - crate::types::api::api_keys::ApiKeyExpiration, - crate::types::api::api_keys::CreateApiKeyRequest, - crate::types::api::api_keys::CreateApiKeyResponse, - crate::types::api::api_keys::RetrieveApiKeyResponse, - crate::types::api::api_keys::RevokeApiKeyResponse, - crate::types::api::api_keys::UpdateApiKeyRequest, + api_models::admin::MerchantAccountResponse, + api_models::admin::MerchantConnectorId, + api_models::admin::MerchantDetails, + api_models::admin::WebhookDetails, + api_models::api_keys::ApiKeyExpiration, + api_models::api_keys::CreateApiKeyRequest, + api_models::api_keys::CreateApiKeyResponse, + api_models::api_keys::RetrieveApiKeyResponse, + api_models::api_keys::RevokeApiKeyResponse, + api_models::api_keys::UpdateApiKeyRequest, api_models::payments::RetrievePaymentLinkRequest, api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, + api_models::routing::RoutingConfigRequest, + api_models::routing::RoutingDictionaryRecord, + api_models::routing::RoutingKind, + api_models::routing::RoutableConnectorChoice, + api_models::routing::LinkedRoutingConfigRetrieveResponse, + api_models::routing::RoutingRetrieveResponse, + api_models::routing::ProfileDefaultRoutingConfig, + api_models::routing::MerchantRoutingAlgorithm, + api_models::routing::RoutingAlgorithmKind, + api_models::routing::RoutingDictionary, + api_models::routing::RoutingAlgorithm, + api_models::routing::StraightThroughAlgorithm, + api_models::routing::ConnectorVolumeSplit, + api_models::routing::ConnectorSelection, + api_models::routing::ast::RoutableChoiceKind, + api_models::enums::RoutableConnectors, + api_models::routing::ast::ProgramConnectorSelection, + api_models::routing::ast::RuleConnectorSelection, + api_models::routing::ast::IfStatement, + api_models::routing::ast::Comparison, + api_models::routing::ast::ComparisonType, + api_models::routing::ast::ValueType, + api_models::routing::ast::MetadataValue, + api_models::routing::ast::NumberComparison, + api_models::payment_methods::RequestPaymentMethodTypes, api_models::payments::PaymentLinkStatus, api_models::blocklist::BlocklistRequest, api_models::blocklist::BlocklistResponse, api_models::blocklist::ListBlocklistQuery, - common_enums::enums::BlocklistDataKind + api_models::enums::BlocklistDataKind )), modifiers(&SecurityAddon) )] @@ -395,8 +467,7 @@ impl utoipa::Modify for SecurityAddon { "api_key", SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::with_description( "api-key", - "API keys are the most common method of authentication and can be obtained \ - from the HyperSwitch dashboard." + "Use the API key created under your merchant account from the HyperSwitch dashboard. API key is used to authenticate API requests from your merchant server only. Don't expose this key on a website or embed it in a mobile application." ))), ), ( @@ -427,107 +498,3 @@ impl utoipa::Modify for SecurityAddon { } } } - -pub mod examples { - /// Creating the payment with minimal fields - pub const PAYMENTS_CREATE_MINIMUM_FIELDS: &str = r#"{ - "amount": 6540, - "currency": "USD", - }"#; - - /// Creating a manual capture payment - pub const PAYMENTS_CREATE_WITH_MANUAL_CAPTURE: &str = r#"{ - "amount": 6540, - "currency": "USD", - "capture_method":"manual" - }"#; - - /// Creating a payment with billing and shipping address - pub const PAYMENTS_CREATE_WITH_ADDRESS: &str = r#"{ - "amount": 6540, - "currency": "USD", - "customer": { - "id" : "cus_abcdefgh" - }, - "billing": { - "address": { - "line1": "1467", - "line2": "Harrison Street", - "line3": "Harrison Street", - "city": "San Fransico", - "state": "California", - "zip": "94122", - "country": "US", - "first_name": "joseph", - "last_name": "Doe" - }, - "phone": { - "number": "8056594427", - "country_code": "+91" - } - } - }"#; - - /// Creating a payment with customer details - pub const PAYMENTS_CREATE_WITH_CUSTOMER_DATA: &str = r#"{ - "amount": 6540, - "currency": "USD", - "customer": { - "id":"cus_abcdefgh", - "name":"John Dough", - "phone":"9999999999", - "email":"john@example.com" - } - }"#; - - /// 3DS force payment - pub const PAYMENTS_CREATE_WITH_FORCED_3DS: &str = r#"{ - "amount": 6540, - "currency": "USD", - "authentication_type" : "three_ds" - }"#; - - /// A payment with other fields - pub const PAYMENTS_CREATE: &str = r#"{ - "amount": 6540, - "currency": "USD", - "payment_id": "abcdefghijklmnopqrstuvwxyz", - "customer": { - "id":"cus_abcdefgh", - "name":"John Dough", - "phone":"9999999999", - "email":"john@example.com" - }, - "description": "Its my first payment request", - "statement_descriptor_name": "joseph", - "statement_descriptor_suffix": "JS", - "metadata": { - "udf1": "some-value", - "udf2": "some-value" - } - }"#; - - /// Creating the payment with order details - pub const PAYMENTS_CREATE_WITH_ORDER_DETAILS: &str = r#"{ - "amount": 6540, - "currency": "USD", - "order_details": [ - { - "product_name": "Apple iPhone 15", - "quantity": 1, - "amount" : 6540 - } - ] - }"#; - - /// Creating the payment with connector metadata for noon - pub const PAYMENTS_CREATE_WITH_NOON_ORDER_CATETORY: &str = r#"{ - "amount": 6540, - "currency": "USD", - "connector_metadata": { - "noon": { - "order_category":"shoes" - } - } - }"#; -} diff --git a/crates/openapi/src/routes.rs b/crates/openapi/src/routes.rs new file mode 100644 index 000000000000..5a1822959556 --- /dev/null +++ b/crates/openapi/src/routes.rs @@ -0,0 +1,26 @@ +#![allow(unused)] + +pub mod api_keys; +pub mod blocklist; +pub mod business_profile; +pub mod customers; +pub mod disputes; +pub mod gsm; +pub mod mandates; +pub mod merchant_account; +pub mod merchant_connector_account; +pub mod payment_link; +pub mod payment_method; +pub mod payments; +pub mod payouts; +pub mod refunds; +pub mod routing; + +pub use customers::*; +pub use mandates::*; +pub use merchant_account::*; +pub use merchant_connector_account::*; +pub use payment_method::*; +pub use payments::*; +pub use refunds::*; +pub use routing::*; diff --git a/crates/openapi/src/routes/api_keys.rs b/crates/openapi/src/routes/api_keys.rs new file mode 100644 index 000000000000..2956569bfb0e --- /dev/null +++ b/crates/openapi/src/routes/api_keys.rs @@ -0,0 +1,80 @@ +/// API Key - Create +/// +/// Create a new API Key for accessing our APIs from your servers. The plaintext API Key will be +/// displayed only once on creation, so ensure you store it securely. +#[utoipa::path( + post, + path = "/api_keys/{merchant_id)", + params(("merchant_id" = String, Path, description = "The unique identifier for the merchant account")), + request_body= CreateApiKeyRequest, + responses( + (status = 200, description = "API Key created", body = CreateApiKeyResponse), + (status = 400, description = "Invalid data") + ), + tag = "API Key", + operation_id = "Create an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_create() {} + +/// API Key - Retrieve +/// +/// Retrieve information about the specified API Key. +#[utoipa::path( + get, + path = "/api_keys/{merchant_id}/{key_id}", + params ( + ("merchant_id" = String, Path, description = "The unique identifier for the merchant account"), + ("key_id" = String, Path, description = "The unique identifier for the API Key") + ), + responses( + (status = 200, description = "API Key retrieved", body = RetrieveApiKeyResponse), + (status = 404, description = "API Key not found") + ), + tag = "API Key", + operation_id = "Retrieve an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_retrieve() {} + +/// API Key - Update +/// +/// Update information for the specified API Key. +#[utoipa::path( + post, + path = "/api_keys/{merchant_id}/{key_id}", + request_body = UpdateApiKeyRequest, + params ( + ("merchant_id" = String, Path, description = "The unique identifier for the merchant account"), + ("key_id" = String, Path, description = "The unique identifier for the API Key") + ), + responses( + (status = 200, description = "API Key updated", body = RetrieveApiKeyResponse), + (status = 404, description = "API Key not found") + ), + tag = "API Key", + operation_id = "Update an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_update() {} + +/// API Key - Revoke +/// +/// Revoke the specified API Key. Once revoked, the API Key can no longer be used for +/// authenticating with our APIs. +#[utoipa::path( + delete, + path = "/api_keys/{merchant_id)/{key_id}", + params ( + ("merchant_id" = String, Path, description = "The unique identifier for the merchant account"), + ("key_id" = String, Path, description = "The unique identifier for the API Key") + ), + responses( + (status = 200, description = "API Key revoked", body = RevokeApiKeyResponse), + (status = 404, description = "API Key not found") + ), + tag = "API Key", + operation_id = "Revoke an API Key", + security(("admin_api_key" = [])) +)] +pub async fn api_key_revoke() {} diff --git a/crates/openapi/src/routes/blocklist.rs b/crates/openapi/src/routes/blocklist.rs new file mode 100644 index 000000000000..bf683259e081 --- /dev/null +++ b/crates/openapi/src/routes/blocklist.rs @@ -0,0 +1,43 @@ +#[utoipa::path( + post, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Blocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Block a Fingerprint", + security(("api_key" = [])) +)] +pub async fn add_entry_to_blocklist() {} + +#[utoipa::path( + delete, + path = "/blocklist", + request_body = BlocklistRequest, + responses( + (status = 200, description = "Fingerprint Unblocked", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "Unblock a Fingerprint", + security(("api_key" = [])) +)] +pub async fn remove_entry_from_blocklist() {} + +#[utoipa::path( + get, + path = "/blocklist", + params ( + ("data_kind" = BlocklistDataKind, Query, description = "Kind of the fingerprint list requested"), + ), + responses( + (status = 200, description = "Blocked Fingerprints", body = BlocklistResponse), + (status = 400, description = "Invalid Data") + ), + tag = "Blocklist", + operation_id = "List Blocked fingerprints of a particular kind", + security(("api_key" = [])) +)] +pub async fn list_blocked_payment_methods() {} diff --git a/crates/openapi/src/routes/business_profile.rs b/crates/openapi/src/routes/business_profile.rs new file mode 100644 index 000000000000..57da0e59701c --- /dev/null +++ b/crates/openapi/src/routes/business_profile.rs @@ -0,0 +1,124 @@ +/// Business Profile - Create +/// +/// Creates a new *business profile* for a merchant +#[utoipa::path( + post, + path = "/account/{account_id}/business_profile", + params ( + ("account_id" = String, Path, description = "The unique identifier for the merchant account") + ), + request_body( + content = BusinessProfileCreate, + examples( + ( + "Create a business profile with minimal fields" = ( + value = json!({}) + ) + ), + ( + "Create a business profile with profile name" = ( + value = json!({ + "profile_name": "shoe_business" + }) + ) + ) + ) + ), + responses( + (status = 200, description = "Business Account Created", body = BusinessProfileResponse), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Create A Business Profile", + security(("admin_api_key" = [])) +)] +pub async fn business_profile_create() {} + +/// Business Profile - List +/// +/// Lists all the *business profiles* under a merchant +#[utoipa::path( + get, + path = "/account/{account_id}/business_profile", + params ( + ("account_id" = String, Path, description = "Merchant Identifier"), + ), + responses( + (status = 200, description = "Business profiles Retrieved", body = Vec) + ), + tag = "Business Profile", + operation_id = "List Business Profiles", + security(("api_key" = [])) +)] +pub async fn business_profiles_list() {} + +/// Business Profile - Update +/// +/// Update the *business profile* +#[utoipa::path( + post, + path = "/account/{account_id}/business_profile/{profile_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("profile_id" = String, Path, description = "The unique identifier for the business profile") + ), + request_body( + content = BusinessProfileCreate, + examples( + ( + "Update business profile with profile name fields" = ( + value = json!({ + "profile_name" : "shoe_business" + }) + ) + ) + )), + responses( + (status = 200, description = "Business Profile Updated", body = BusinessProfileResponse), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Update a Business Profile", + security(("api_key" = [])) +)] +pub async fn business_profiles_update() {} + +/// Business Profile - Delete +/// +/// Delete the *business profile* +#[utoipa::path( + delete, + path = "/account/{account_id}/business_profile/{profile_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("profile_id" = String, Path, description = "The unique identifier for the business profile") + ), + responses( + (status = 200, description = "Business profiles Deleted", body = bool), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Delete the Business Profile", + security(("api_key" = [])) +)] +pub async fn business_profiles_delete() {} + +/// Business Profile - Retrieve +/// +/// Retrieve existing *business profile* +#[utoipa::path( + get, + path = "/account/{account_id}/business_profile/{profile_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("profile_id" = String, Path, description = "The unique identifier for the business profile") + ), + responses( + (status = 200, description = "Business Profile Updated", body = BusinessProfileResponse), + (status = 400, description = "Invalid data") + ), + tag = "Business Profile", + operation_id = "Retrieve a Business Profile", + security(("api_key" = [])) +)] +pub async fn business_profiles_retrieve() {} diff --git a/crates/openapi/src/routes/customers.rs b/crates/openapi/src/routes/customers.rs new file mode 100644 index 000000000000..7517cdc61a04 --- /dev/null +++ b/crates/openapi/src/routes/customers.rs @@ -0,0 +1,102 @@ +/// Customers - Create +/// +/// Creates a customer object and stores the customer details to be reused for future payments. +/// Incase the customer already exists in the system, this API will respond with the customer details. +#[utoipa::path( + post, + path = "/customers", + request_body ( + content = CustomerRequest, + examples (( "Update name and email of a customer" =( + value =json!( { + "email": "guest@example.com", + "name": "John Doe" + }) + ))) + ), + responses( + (status = 200, description = "Customer Created", body = CustomerResponse), + (status = 400, description = "Invalid data") + + ), + tag = "Customers", + operation_id = "Create a Customer", + security(("api_key" = [])) +)] +pub async fn customers_create() {} + +/// Customers - Retrieve +/// +/// Retrieves a customer's details. +#[utoipa::path( + get, + path = "/customers/{customer_id}", + params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), + responses( + (status = 200, description = "Customer Retrieved", body = CustomerResponse), + (status = 404, description = "Customer was not found") + ), + tag = "Customers", + operation_id = "Retrieve a Customer", + security(("api_key" = []), ("ephemeral_key" = [])) +)] +pub async fn customers_retrieve() {} + +/// Customers - Update +/// +/// Updates the customer's details in a customer object. +#[utoipa::path( + post, + path = "/customers/{customer_id}", + request_body ( + content = CustomerRequest, + examples (( "Update name and email of a customer" =( + value =json!( { + "email": "guest@example.com", + "name": "John Doe" + }) + ))) + ), + params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), + responses( + (status = 200, description = "Customer was Updated", body = CustomerResponse), + (status = 404, description = "Customer was not found") + ), + tag = "Customers", + operation_id = "Update a Customer", + security(("api_key" = [])) +)] +pub async fn customers_update() {} + +/// Customers - Delete +/// +/// Delete a customer record. +#[utoipa::path( + delete, + path = "/customers/{customer_id}", + params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), + responses( + (status = 200, description = "Customer was Deleted", body = CustomerDeleteResponse), + (status = 404, description = "Customer was not found") + ), + tag = "Customers", + operation_id = "Delete a Customer", + security(("api_key" = [])) +)] +pub async fn customers_delete() {} + +/// Customers - List +/// +/// Lists all the customers for a particular merchant id. +#[utoipa::path( + post, + path = "/customers/list", + responses( + (status = 200, description = "Customers retrieved", body = Vec), + (status = 400, description = "Invalid Data"), + ), + tag = "Customers List", + operation_id = "List all Customers for a Merchant", + security(("api_key" = [])) +)] +pub async fn customers_list() {} diff --git a/crates/openapi/src/routes/disputes.rs b/crates/openapi/src/routes/disputes.rs new file mode 100644 index 000000000000..491edae41010 --- /dev/null +++ b/crates/openapi/src/routes/disputes.rs @@ -0,0 +1,44 @@ +/// Disputes - Retrieve Dispute +/// Retrieves a dispute +#[utoipa::path( + get, + path = "/disputes/{dispute_id}", + params( + ("dispute_id" = String, Path, description = "The identifier for dispute") + ), + responses( + (status = 200, description = "The dispute was retrieved successfully", body = DisputeResponse), + (status = 404, description = "Dispute does not exist in our records") + ), + tag = "Disputes", + operation_id = "Retrieve a Dispute", + security(("api_key" = [])) +)] +pub async fn retrieve_dispute() {} + +/// Disputes - List Disputes +/// Lists all the Disputes for a merchant +#[utoipa::path( + get, + path = "/disputes/list", + params( + ("limit" = Option, Query, description = "The maximum number of Dispute Objects to include in the response"), + ("dispute_status" = Option, Query, description = "The status of dispute"), + ("dispute_stage" = Option, Query, description = "The stage of dispute"), + ("reason" = Option, Query, description = "The reason for dispute"), + ("connector" = Option, Query, description = "The connector linked to dispute"), + ("received_time" = Option, Query, description = "The time at which dispute is received"), + ("received_time.lt" = Option, Query, description = "Time less than the dispute received time"), + ("received_time.gt" = Option, Query, description = "Time greater than the dispute received time"), + ("received_time.lte" = Option, Query, description = "Time less than or equals to the dispute received time"), + ("received_time.gte" = Option, Query, description = "Time greater than or equals to the dispute received time"), + ), + responses( + (status = 200, description = "The dispute list was retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized request") + ), + tag = "Disputes", + operation_id = "List Disputes", + security(("api_key" = [])) +)] +pub async fn retrieve_disputes_list() {} diff --git a/crates/openapi/src/routes/gsm.rs b/crates/openapi/src/routes/gsm.rs new file mode 100644 index 000000000000..f20cc1f261a6 --- /dev/null +++ b/crates/openapi/src/routes/gsm.rs @@ -0,0 +1,75 @@ +/// Gsm - Create +/// +/// Creates a GSM (Global Status Mapping) Rule. A GSM rule is used to map a connector's error message/error code combination during a particular payments flow/sub-flow to Hyperswitch's unified status/error code/error message combination. It is also used to decide the next action in the flow - retry/requeue/do_default +#[utoipa::path( + post, + path = "/gsm", + request_body( + content = GsmCreateRequest, + ), + responses( + (status = 200, description = "Gsm created", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Create Gsm Rule", + security(("admin_api_key" = [])), +)] +pub async fn create_gsm_rule() {} + +/// Gsm - Get +/// +/// Retrieves a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/get", + request_body( + content = GsmRetrieveRequest, + ), + responses( + (status = 200, description = "Gsm retrieved", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Retrieve Gsm Rule", + security(("admin_api_key" = [])), +)] +pub async fn get_gsm_rule() {} + +/// Gsm - Update +/// +/// Updates a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/update", + request_body( + content = GsmUpdateRequest, + ), + responses( + (status = 200, description = "Gsm updated", body = GsmResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Update Gsm Rule", + security(("admin_api_key" = [])), +)] +pub async fn update_gsm_rule() {} + +/// Gsm - Delete +/// +/// Deletes a Gsm Rule +#[utoipa::path( + post, + path = "/gsm/delete", + request_body( + content = GsmDeleteRequest, + ), + responses( + (status = 200, description = "Gsm deleted", body = GsmDeleteResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Gsm", + operation_id = "Delete Gsm Rule", + security(("admin_api_key" = [])), +)] +pub async fn delete_gsm_rule() {} diff --git a/crates/openapi/src/routes/mandates.rs b/crates/openapi/src/routes/mandates.rs new file mode 100644 index 000000000000..c61e2eb2f560 --- /dev/null +++ b/crates/openapi/src/routes/mandates.rs @@ -0,0 +1,61 @@ +/// Mandates - Retrieve Mandate +/// +/// Retrieves a mandate created using the Payments/Create API +#[utoipa::path( + get, + path = "/mandates/{mandate_id}", + params( + ("mandate_id" = String, Path, description = "The identifier for mandate") + ), + responses( + (status = 200, description = "The mandate was retrieved successfully", body = MandateResponse), + (status = 404, description = "Mandate does not exist in our records") + ), + tag = "Mandates", + operation_id = "Retrieve a Mandate", + security(("api_key" = [])) +)] +pub async fn get_mandate() {} + +/// Mandates - Revoke Mandate +/// +/// Revokes a mandate created using the Payments/Create API +#[utoipa::path( + post, + path = "/mandates/revoke/{mandate_id}", + params( + ("mandate_id" = String, Path, description = "The identifier for a mandate") + ), + responses( + (status = 200, description = "The mandate was revoked successfully", body = MandateRevokedResponse), + (status = 400, description = "Mandate does not exist in our records") + ), + tag = "Mandates", + operation_id = "Revoke a Mandate", + security(("api_key" = [])) +)] +pub async fn revoke_mandate() {} + +/// Mandates - List Mandates +#[utoipa::path( + get, + path = "/mandates/list", + params( + ("limit" = Option, Query, description = "The maximum number of Mandate Objects to include in the response"), + ("mandate_status" = Option, Query, description = "The status of mandate"), + ("connector" = Option, Query, description = "The connector linked to mandate"), + ("created_time" = Option, Query, description = "The time at which mandate is created"), + ("created_time.lt" = Option, Query, description = "Time less than the mandate created time"), + ("created_time.gt" = Option, Query, description = "Time greater than the mandate created time"), + ("created_time.lte" = Option, Query, description = "Time less than or equals to the mandate created time"), + ("created_time.gte" = Option, Query, description = "Time greater than or equals to the mandate created time"), + ), + responses( + (status = 200, description = "The mandate list was retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized request") + ), + tag = "Mandates", + operation_id = "List Mandates", + security(("api_key" = [])) +)] +pub async fn retrieve_mandates_list() {} diff --git a/crates/openapi/src/routes/merchant_account.rs b/crates/openapi/src/routes/merchant_account.rs new file mode 100644 index 000000000000..967e28225c2a --- /dev/null +++ b/crates/openapi/src/routes/merchant_account.rs @@ -0,0 +1,125 @@ +/// Merchant Account - Create +/// +/// Create a new account for a *merchant* and the *merchant* could be a seller or retailer or client who likes to receive and send payments. +#[utoipa::path( + post, + path = "/accounts", + request_body( + content = MerchantAccountCreate, + examples( + ( + "Create a merchant account with minimal fields" = ( + value = json!({"merchant_id": "merchant_abc"}) + ) + ), + ( + "Create a merchant account with webhook url" = ( + value = json!({ + "merchant_id": "merchant_abc", + "webhook_details" : { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + }) + ) + ), + ( + "Create a merchant account with return url" = ( + value = json!({"merchant_id": "merchant_abc", + "return_url": "https://example.com"}) + ) + ) + ) + + ), + responses( + (status = 200, description = "Merchant Account Created", body = MerchantAccountResponse), + (status = 400, description = "Invalid data") + ), + tag = "Merchant Account", + operation_id = "Create a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn merchant_account_create() {} + +/// Merchant Account - Retrieve +/// +/// Retrieve a *merchant* account details. +#[utoipa::path( + get, + path = "/accounts/{account_id}", + params (("account_id" = String, Path, description = "The unique identifier for the merchant account")), + responses( + (status = 200, description = "Merchant Account Retrieved", body = MerchantAccountResponse), + (status = 404, description = "Merchant account not found") + ), + tag = "Merchant Account", + operation_id = "Retrieve a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn retrieve_merchant_account() {} + +/// Merchant Account - Update +/// +/// Updates details of an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc +#[utoipa::path( + post, + path = "/accounts/{account_id}", + request_body ( + content = MerchantAccountUpdate, + examples( + ( + "Update merchant name" = ( + value = json!({ + "merchant_id": "merchant_abc", + "merchant_name": "merchant_name" + }) + ) + ), + ("Update merchant name" = ( + value = json!({ + "merchant_id": "merchant_abc", + "merchant_name": "merchant_name" + }) + )), + ("Update webhook url" = ( + value = json!({ + "merchant_id": "merchant_abc", + "webhook_details": { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + }) + ) + ), + ("Update return url" = ( + value = json!({ + "merchant_id": "merchant_abc", + "return_url": "https://example.com" + }) + )))), + params (("account_id" = String, Path, description = "The unique identifier for the merchant account")), + responses( + (status = 200, description = "Merchant Account Updated", body = MerchantAccountResponse), + (status = 404, description = "Merchant account not found") + ), + tag = "Merchant Account", + operation_id = "Update a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn update_merchant_account() {} + +/// Merchant Account - Delete +/// +/// Delete a *merchant* account +#[utoipa::path( + delete, + path = "/accounts/{account_id}", + params (("account_id" = String, Path, description = "The unique identifier for the merchant account")), + responses( + (status = 200, description = "Merchant Account Deleted", body = MerchantAccountDeleteResponse), + (status = 404, description = "Merchant account not found") + ), + tag = "Merchant Account", + operation_id = "Delete a Merchant Account", + security(("admin_api_key" = [])) +)] +pub async fn delete_merchant_account() {} diff --git a/crates/openapi/src/routes/merchant_connector_account.rs b/crates/openapi/src/routes/merchant_connector_account.rs new file mode 100644 index 000000000000..6f7746ccd456 --- /dev/null +++ b/crates/openapi/src/routes/merchant_connector_account.rs @@ -0,0 +1,170 @@ +/// Merchant Connector - Create +/// +/// Creates a new Merchant Connector for the merchant account. The connector could be a payment processor/facilitator/acquirer or a provider of specialized services like Fraud/Accounting etc. +#[utoipa::path( + post, + path = "/accounts/{account_id}/connectors", + request_body( + content = MerchantConnectorCreate, + examples( + ( + "Create a merchant connector account with minimal fields" = ( + value = json!({ + "connector_type": "fiz_operations", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{adyen-api-key}}", + "key1": "{{adyen_merchant_account}}" + } + }) + ) + ), + ( + "Create a merchant connector account under a specific business profile" = ( + value = json!({ + "connector_type": "fiz_operations", + "connector_name": "adyen", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{adyen-api-key}}", + "key1": "{{adyen_merchant_account}}" + }, + "profile_id": "{{profile_id}}" + }) + ) + ), + ( + "Create a merchant account with custom connector label" = ( + value = json!({ + "connector_type": "fiz_operations", + "connector_name": "adyen", + "connector_label": "EU_adyen", + "connector_account_details": { + "auth_type": "BodyKey", + "api_key": "{{adyen-api-key}}", + "key1": "{{adyen_merchant_account}}" + } + }) + ) + ), + ) + ), + responses( + (status = 200, description = "Merchant Connector Created", body = MerchantConnectorResponse), + (status = 400, description = "Missing Mandatory fields"), + ), + tag = "Merchant Connector Account", + operation_id = "Create a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_create() {} + +/// Merchant Connector - Retrieve +/// +/// Retrieves details of a Connector account +#[utoipa::path( + get, + path = "/accounts/{account_id}/connectors/{connector_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("connector_id" = i32, Path, description = "The unique identifier for the Merchant Connector") + ), + responses( + (status = 200, description = "Merchant Connector retrieved successfully", body = MerchantConnectorResponse), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "Retrieve a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_retrieve() {} + +/// Merchant Connector - List +/// +/// List Merchant Connector Details for the merchant +#[utoipa::path( + get, + path = "/accounts/{account_id}/connectors", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ), + responses( + (status = 200, description = "Merchant Connector list retrieved successfully", body = Vec), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "List all Merchant Connectors", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_list() {} + +/// Merchant Connector - Update +/// +/// To update an existing Merchant Connector account. Helpful in enabling/disabling different payment methods and other settings for the connector +#[utoipa::path( + post, + path = "/accounts/{account_id}/connectors/{connector_id}", + request_body( + content = MerchantConnectorUpdate, + examples( + ( + "Enable card payment method" = ( + value = json! ({ + "connector_type": "fiz_operations", + "payment_methods_enabled": [ + { + "payment_method": "card" + } + ] + }) + ) + ), + ( + "Update webhook secret" = ( + value = json! ({ + "connector_webhook_details": { + "merchant_secret": "{{webhook_secret}}" + } + }) + ) + ) + ), + ), + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("connector_id" = i32, Path, description = "The unique identifier for the Merchant Connector") + ), + responses( + (status = 200, description = "Merchant Connector Updated", body = MerchantConnectorResponse), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "Update a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_update() {} + +/// Merchant Connector - Delete +/// +/// Delete or Detach a Merchant Connector from Merchant Account +#[utoipa::path( + delete, + path = "/accounts/{account_id}/connectors/{connector_id}", + params( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("connector_id" = i32, Path, description = "The unique identifier for the Merchant Connector") + ), + responses( + (status = 200, description = "Merchant Connector Deleted", body = MerchantConnectorDeleteResponse), + (status = 404, description = "Merchant Connector does not exist in records"), + (status = 401, description = "Unauthorized request") + ), + tag = "Merchant Connector Account", + operation_id = "Delete a Merchant Connector", + security(("admin_api_key" = [])) +)] +pub async fn payment_connector_delete() {} diff --git a/crates/openapi/src/routes/payment_link.rs b/crates/openapi/src/routes/payment_link.rs new file mode 100644 index 000000000000..9b9cb3a4d529 --- /dev/null +++ b/crates/openapi/src/routes/payment_link.rs @@ -0,0 +1,19 @@ +/// Payments Link - Retrieve +/// +/// To retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/payment_link/{payment_link_id}", + params( + ("payment_link_id" = String, Path, description = "The identifier for payment link") + ), + request_body=RetrievePaymentLinkRequest, + responses( + (status = 200, description = "Gets details regarding payment link", body = RetrievePaymentLinkResponse), + (status = 404, description = "No payment link found") + ), + tag = "Payments", + operation_id = "Retrieve a Payment Link", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub async fn payment_link_retrieve() {} diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs new file mode 100644 index 000000000000..ffb9d8c5f7e5 --- /dev/null +++ b/crates/openapi/src/routes/payment_method.rs @@ -0,0 +1,173 @@ +/// PaymentMethods - Create +/// +/// Creates and stores a payment method against a customer. +/// In case of cards, this API should be used only by PCI compliant merchants. +#[utoipa::path( + post, + path = "/payment_methods", + request_body ( + content = PaymentMethodCreate, + examples (( "Save a card" =( + value =json!( { + "payment_method": "card", + "payment_method_type": "credit", + "payment_method_issuer": "Visa", + "card": { + "card_number": "4242424242424242", + "card_exp_month": "11", + "card_exp_year": "25", + "card_holder_name": "John Doe" + }, + "customer_id": "{{customer_id}}" + }) + ))) + ), + responses( + (status = 200, description = "Payment Method Created", body = PaymentMethodResponse), + (status = 400, description = "Invalid Data") + + ), + tag = "Payment Methods", + operation_id = "Create a Payment Method", + security(("api_key" = [])) +)] +pub async fn create_payment_method_api() {} + +/// List payment methods for a Merchant +/// +/// Lists the applicable payment methods for a particular Merchant ID. +/// Use the client secret and publishable key authorization to list all relevant payment methods of the merchant for the payment corresponding to the client secret. +#[utoipa::path( + get, + path = "/account/payment_methods", + params ( + ("account_id" = String, Path, description = "The unique identifier for the merchant account"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved", body = PaymentMethodListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Merchant", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub async fn list_payment_method_api() {} + +/// List payment methods for a Customer +/// +/// Lists all the applicable payment methods for a particular Customer ID. +#[utoipa::path( + get, + path = "/customers/{customer_id}/payment_methods", + params ( + ("customer_id" = String, Path, description = "The unique identifier for the customer account"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("api_key" = [])) +)] +pub async fn list_customer_payment_method_api() {} + +/// List payment methods for a Payment +/// +/// Lists all the applicable payment methods for a particular payment tied to the `client_secret`. +#[utoipa::path( + get, + path = "/customers/payment_methods", + params ( + ("client-secret" = String, Path, description = "A secret known only to your client and the authorization server. Used for client side authentication"), + ("customer_id" = String, Path, description = "The unique identifier for the customer account"), + ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), + ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), + ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), + ("maximum_amount" = i64, Query, description = "The maximum amount accepted for processing by the particular payment method."), + ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), + ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), + ), + responses( + (status = 200, description = "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", body = CustomerPaymentMethodsListResponse), + (status = 400, description = "Invalid Data"), + (status = 404, description = "Payment Methods does not exist in records") + ), + tag = "Payment Methods", + operation_id = "List all Payment Methods for a Customer", + security(("publishable_key" = [])) +)] +pub async fn list_customer_payment_method_api_client() {} + +/// Payment Method - Retrieve +/// +/// Retrieves a payment method of a customer. +#[utoipa::path( + get, + path = "/payment_methods/{method_id}", + params ( + ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + responses( + (status = 200, description = "Payment Method retrieved", body = PaymentMethodResponse), + (status = 404, description = "Payment Method does not exist in records") + ), + tag = "Payment Methods", + operation_id = "Retrieve a Payment method", + security(("api_key" = [])) +)] +pub async fn payment_method_retrieve_api() {} + +/// Payment Method - Update +/// +/// Update an existing payment method of a customer. +/// This API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments. +#[utoipa::path( + post, + path = "/payment_methods/{method_id}", + params ( + ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + request_body = PaymentMethodUpdate, + responses( + (status = 200, description = "Payment Method updated", body = PaymentMethodResponse), + (status = 404, description = "Payment Method does not exist in records") + ), + tag = "Payment Methods", + operation_id = "Update a Payment method", + security(("api_key" = [])) +)] +pub async fn payment_method_update_api() {} + +/// Payment Method - Delete +/// +/// Deletes a payment method of a customer. +#[utoipa::path( + delete, + path = "/payment_methods/{method_id}", + params ( + ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), + ), + responses( + (status = 200, description = "Payment Method deleted", body = PaymentMethodDeleteResponse), + (status = 404, description = "Payment Method does not exist in records") + ), + tag = "Payment Methods", + operation_id = "Delete a Payment method", + security(("api_key" = [])) +)] +pub async fn payment_method_delete_api() {} diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs new file mode 100644 index 000000000000..b6c83d5aae7c --- /dev/null +++ b/crates/openapi/src/routes/payments.rs @@ -0,0 +1,452 @@ +/// Payments - Create +/// +/// **Creates a payment object when amount and currency are passed.** This API is also used to create a mandate by passing the `mandate_object`. +/// +/// To completely process a payment you will have to create a payment, attach a payment method, confirm and capture funds. +/// +/// Depending on the user journey you wish to achieve, you may opt to complete all the steps in a single request by attaching a payment method, setting `confirm=true` and `capture_method = automatic` in the *Payments/Create API* request or you could use the following sequence of API requests to achieve the same: +/// +/// 1. Payments - Create +/// +/// 2. Payments - Update +/// +/// 3. Payments - Confirm +/// +/// 4. Payments - Capture. +/// +/// Use the client secret returned in this API along with your publishable key to make subsequent API calls from your client +#[utoipa::path( + post, + path = "/payments", + request_body( + content = PaymentsCreateRequest, + examples( + ( + "Create a payment with minimal fields" = ( + value = json!({"amount": 6540,"currency": "USD"}) + ) + ), + ( + "Create a payment with customer details and metadata" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "payment_id": "abcdefghijklmnopqrstuvwxyz", + "customer": { + "id": "cus_abcdefgh", + "name": "John Dough", + "phone": "9999999999", + "email": "john@example.com" + }, + "description": "Its my first payment request", + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "some-value", + "udf2": "some-value" + } + }) + ) + ), + ( + "Create a 3DS payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "authentication_type": "three_ds" + }) + ) + ), + ( + "Create a manual capture payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "capture_method": "manual" + }) + ) + ), + ( + "Create a setup mandate payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "customer_id": "StripeCustomer123", + "authentication_type": "no_three_ds", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "setup_future_usage": "off_session", + "mandate_data": { + "customer_acceptance": { + "acceptance_type": "offline", + "accepted_at": "1963-05-03T04:07:52.723Z", + "online": { + "ip_address": "127.0.0.1", + "user_agent": "amet irure esse" + } + }, + "mandate_type": { + "single_use": { + "amount": 6540, + "currency": "USD" + } + } + } + }) + ) + ), + ( + "Create a recurring payment with mandate_id" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "customer_id": "StripeCustomer", + "authentication_type": "no_three_ds", + "mandate_id": "{{mandate_id}}", + "off_session": true + }) + ) + ), + ( + "Create a payment and save the card" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "customer_id": "StripeCustomer123", + "authentication_type": "no_three_ds", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "setup_future_usage": "off_session" + }) + ) + ), + ( + "Create a payment using an already saved card's token" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "confirm": true, + "client_secret": "{{client_secret}}", + "payment_method": "card", + "payment_token": "{{payment_token}}", + "card_cvc": "123" + }) + ) + ), + ( + "Create a manual capture payment" = ( + value = json!({ + "amount": 6540, + "currency": "USD", + "customer": { + "id": "cus_abcdefgh" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "joseph", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + } + }) + ) + ) + ), + ), + responses( + (status = 200, description = "Payment created", body = PaymentsResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payments", + operation_id = "Create a Payment", + security(("api_key" = [])), +)] +pub fn payments_create() {} + +/// Payments - Retrieve +/// +/// Retrieves a Payment. This API can also be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/payments/{payment_id}", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body=PaymentRetrieveBody, + responses( + (status = 200, description = "Gets the payment with final status", body = PaymentsResponse), + (status = 404, description = "No payment found") + ), + tag = "Payments", + operation_id = "Retrieve a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub fn payments_retrieve() {} + +/// Payments - Update +/// +/// To update the properties of a *PaymentIntent* object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created +#[utoipa::path( + post, + path = "/payments/{payment_id}", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body( + content = PaymentsUpdateRequest, + examples( + ( + "Update the payment amount" = ( + value = json!({ + "amount": 7654, + } + ) + ) + ), + ( + "Update the shipping address" = ( + value = json!( + { + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "joseph", + "last_name": "Doe" + }, + "phone": { + "number": "8056594427", + "country_code": "+91" + } + }, + } + ) + ) + ) + ) + ), + responses( + (status = 200, description = "Payment updated", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Update a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub fn payments_update() {} + +/// Payments - Confirm +/// +/// **Use this API to confirm the payment and forward the payment to the payment processor.** +/// +/// Alternatively you can confirm the payment within the *Payments/Create* API by setting `confirm=true`. After confirmation, the payment could either: +/// +/// 1. fail with `failed` status or +/// +/// 2. transition to a `requires_customer_action` status with a `next_action` block or +/// +/// 3. succeed with either `succeeded` in case of automatic capture or `requires_capture` in case of manual capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/confirm", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body( + content = PaymentsConfirmRequest, + examples( + ( + "Confirm a payment with payment method data" = ( + value = json!({ + "payment_method": "card", + "payment_method_type": "credit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + } + } + ) + ) + ) + ) + ), + responses( + (status = 200, description = "Payment confirmed", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Confirm a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +pub fn payments_confirm() {} + +/// Payments - Capture +/// +/// To capture the funds for an uncaptured payment +#[utoipa::path( + post, + path = "/payments/{payment_id}/capture", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body ( + content = PaymentsCaptureRequest, + examples( + ( + "Capture the full amount" = ( + value = json!({}) + ) + ), + ( + "Capture partial amount" = ( + value = json!({"amount_to_capture": 654}) + ) + ), + ) + ), + responses( + (status = 200, description = "Payment captured", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Capture a Payment", + security(("api_key" = [])) +)] +pub fn payments_capture() {} + +/// Payments - Session token +/// +/// Creates a session object or a session token for wallets like Apple Pay, Google Pay, etc. These tokens are used by Hyperswitch's SDK to initiate these wallets' SDK. +#[utoipa::path( + post, + path = "/payments/session_tokens", + request_body=PaymentsSessionRequest, + responses( + (status = 200, description = "Payment session object created or session token was retrieved from wallets", body = PaymentsSessionResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Create Session tokens for a Payment", + security(("publishable_key" = [])) +)] +pub fn payments_connector_session() {} + +/// Payments - Cancel +/// +/// A Payment could can be cancelled when it is in one of these statuses: `requires_payment_method`, `requires_capture`, `requires_confirmation`, `requires_customer_action`. +#[utoipa::path( + post, + path = "/payments/{payment_id}/cancel", + request_body ( + content = PaymentsCancelRequest, + examples( + ( + "Cancel the payment with minimal fields" = ( + value = json!({}) + ) + ), + ( + "Cancel the payment with cancellation reason" = ( + value = json!({"cancellation_reason": "requested_by_customer"}) + ) + ), + ) + ), + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment canceled"), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Cancel a Payment", + security(("api_key" = [])) +)] +pub fn payments_cancel() {} + +/// Payments - List +/// +/// To list the *payments* +#[utoipa::path( + get, + path = "/payments/list", + params( + ("customer_id" = String, Query, description = "The identifier for the customer"), + ("starting_after" = String, Query, description = "A cursor for use in pagination, fetch the next list after some object"), + ("ending_before" = String, Query, description = "A cursor for use in pagination, fetch the previous list before some object"), + ("limit" = i64, Query, description = "Limit on the number of objects to return"), + ("created" = PrimitiveDateTime, Query, description = "The time at which payment is created"), + ("created_lt" = PrimitiveDateTime, Query, description = "Time less than the payment created time"), + ("created_gt" = PrimitiveDateTime, Query, description = "Time greater than the payment created time"), + ("created_lte" = PrimitiveDateTime, Query, description = "Time less than or equals to the payment created time"), + ("created_gte" = PrimitiveDateTime, Query, description = "Time greater than or equals to the payment created time") + ), + responses( + (status = 200, description = "Successfully retrieved a payment list", body = Vec), + (status = 404, description = "No payments found") + ), + tag = "Payments", + operation_id = "List all Payments", + security(("api_key" = [])) +)] +pub fn payments_list() {} + +/// Payments - Incremental Authorization +/// +/// Authorized amount for a payment can be incremented if it is in status: requires_capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/incremental_authorization", + request_body=PaymentsIncrementalAuthorizationRequest, + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment authorized amount incremented", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Increment authorized amount for a Payment", + security(("api_key" = [])) +)] +pub fn payments_incremental_authorization() {} diff --git a/crates/openapi/src/routes/payouts.rs b/crates/openapi/src/routes/payouts.rs new file mode 100644 index 000000000000..3cc01de9823f --- /dev/null +++ b/crates/openapi/src/routes/payouts.rs @@ -0,0 +1,85 @@ +/// Payouts - Create +#[utoipa::path( + post, + path = "/payouts/create", + request_body=PayoutCreateRequest, + responses( + (status = 200, description = "Payout created", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Create a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_create() {} + +/// Payouts - Retrieve +#[utoipa::path( + get, + path = "/payouts/{payout_id}", + params( + ("payout_id" = String, Path, description = "The identifier for payout]") + ), + responses( + (status = 200, description = "Payout retrieved", body = PayoutCreateResponse), + (status = 404, description = "Payout does not exist in our records") + ), + tag = "Payouts", + operation_id = "Retrieve a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_retrieve() {} + +/// Payouts - Update +#[utoipa::path( + post, + path = "/payouts/{payout_id}", + params( + ("payout_id" = String, Path, description = "The identifier for payout]") + ), + request_body=PayoutCreateRequest, + responses( + (status = 200, description = "Payout updated", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Update a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_update() {} + +/// Payouts - Cancel +#[utoipa::path( + post, + path = "/payouts/{payout_id}/cancel", + params( + ("payout_id" = String, Path, description = "The identifier for payout") + ), + request_body=PayoutActionRequest, + responses( + (status = 200, description = "Payout cancelled", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Cancel a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_cancel() {} + +/// Payouts - Fulfill +#[utoipa::path( + post, + path = "/payouts/{payout_id}/fulfill", + params( + ("payout_id" = String, Path, description = "The identifier for payout") + ), + request_body=PayoutActionRequest, + responses( + (status = 200, description = "Payout fulfilled", body = PayoutCreateResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payouts", + operation_id = "Fulfill a Payout", + security(("api_key" = [])) +)] +pub async fn payouts_fulfill() {} diff --git a/crates/openapi/src/routes/refunds.rs b/crates/openapi/src/routes/refunds.rs new file mode 100644 index 000000000000..aa4728869be9 --- /dev/null +++ b/crates/openapi/src/routes/refunds.rs @@ -0,0 +1,145 @@ +/// Refunds - Create +/// +/// Creates a refund against an already processed payment. In case of some processors, you can even opt to refund only a partial amount multiple times until the original charge amount has been refunded +#[utoipa::path( + post, + path = "/refunds", + request_body( + content = RefundRequest, + examples( + ( + "Create an instant refund to refund the whole amount" = ( + value = json!({ + "payment_id": "{{payment_id}}", + "refund_type": "instant" + }) + ) + ), + ( + "Create an instant refund to refund partial amount" = ( + value = json!({ + "payment_id": "{{payment_id}}", + "refund_type": "instant", + "amount": 654 + }) + ) + ), + ( + "Create an instant refund with reason" = ( + value = json!({ + "payment_id": "{{payment_id}}", + "refund_type": "instant", + "amount": 6540, + "reason": "Customer returned product" + }) + ) + ), + ) + ), + responses( + (status = 200, description = "Refund created", body = RefundResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Refunds", + operation_id = "Create a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_create() {} + +/// Refunds - Retrieve +/// +/// Retrieves a Refund. This may be used to get the status of a previously initiated refund +#[utoipa::path( + get, + path = "/refunds/{refund_id}", + params( + ("refund_id" = String, Path, description = "The identifier for refund") + ), + responses( + (status = 200, description = "Refund retrieved", body = RefundResponse), + (status = 404, description = "Refund does not exist in our records") + ), + tag = "Refunds", + operation_id = "Retrieve a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_retrieve() {} + +/// Refunds - Retrieve (POST) +/// +/// To retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/refunds/sync", + responses( + (status = 200, description = "Refund retrieved", body = RefundResponse), + (status = 404, description = "Refund does not exist in our records") + ), + tag = "Refunds", + operation_id = "Retrieve a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_retrieve_with_body() {} + +/// Refunds - Update +/// +/// Updates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields +#[utoipa::path( + post, + path = "/refunds/{refund_id}", + params( + ("refund_id" = String, Path, description = "The identifier for refund") + ), + request_body( + content = RefundUpdateRequest, + examples( + ( + "Update refund reason" = ( + value = json!({ + "reason": "Paid by mistake" + }) + ) + ), + ) + ), + responses( + (status = 200, description = "Refund updated", body = RefundResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Refunds", + operation_id = "Update a Refund", + security(("api_key" = [])) +)] +pub async fn refunds_update() {} + +/// Refunds - List +/// +/// Lists all the refunds associated with the merchant or a payment_id if payment_id is not provided +#[utoipa::path( + post, + path = "/refunds/list", + request_body=RefundListRequest, + responses( + (status = 200, description = "List of refunds", body = RefundListResponse), + ), + tag = "Refunds", + operation_id = "List all Refunds", + security(("api_key" = [])) +)] +pub fn refunds_list() {} + +/// Refunds - Filter +/// +/// To list the refunds filters associated with list of connectors, currencies and payment statuses +#[utoipa::path( + post, + path = "/refunds/filter", + request_body=TimeRange, + responses( + (status = 200, description = "List of filters", body = RefundListMetaData), + ), + tag = "Refunds", + operation_id = "List all filters for Refunds", + security(("api_key" = [])) +)] +pub async fn refunds_filter_list() {} diff --git a/crates/openapi/src/routes/routing.rs b/crates/openapi/src/routes/routing.rs new file mode 100644 index 000000000000..ecfac39f9e9a --- /dev/null +++ b/crates/openapi/src/routes/routing.rs @@ -0,0 +1,202 @@ +/// Routing - Create +/// +/// Create a routing config +#[utoipa::path( + post, + path = "/routing", + request_body = RoutingConfigRequest, + responses( + (status = 200, description = "Routing config created", body = RoutingDictionaryRecord), + (status = 400, description = "Request body is malformed"), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Create a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_create_config() {} + +/// Routing - Activate config +/// +/// Activate a routing config +#[utoipa::path( + post, + path = "/routing/{algorithm_id}/activate", + params( + ("algorithm_id" = String, Path, description = "The unique identifier for a config"), + ), + responses( + (status = 200, description = "Routing config activated", body = RoutingDictionaryRecord), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 400, description = "Bad request") + ), + tag = "Routing", + operation_id = "Activate a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_link_config() {} + +/// Routing - Retrieve +/// +/// Retrieve a routing algorithm + +#[utoipa::path( + get, + path = "/routing/{algorithm_id}", + params( + ("algorithm_id" = String, Path, description = "The unique identifier for a config"), + ), + responses( + (status = 200, description = "Successfully fetched routing config", body = MerchantRoutingAlgorithm), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 403, description = "Forbidden") + ), + tag = "Routing", + operation_id = "Retrieve a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_config() {} + +/// Routing - List +/// +/// List all routing configs +#[utoipa::path( + get, + path = "/routing", + params( + ("limit" = Option, Query, description = "The number of records to be returned"), + ("offset" = Option, Query, description = "The record offset from which to start gathering of results"), + ("profile_id" = Option, Query, description = "The unique identifier for a merchant profile"), + ), + responses( + (status = 200, description = "Successfully fetched routing configs", body = RoutingKind), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing") + ), + tag = "Routing", + operation_id = "List routing configs", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn list_routing_configs() {} + +/// Routing - Deactivate +/// +/// Deactivates a routing config +#[utoipa::path( + post, + path = "/routing/deactivate", + request_body = RoutingConfigRequest, + responses( + (status = 200, description = "Successfully deactivated routing config", body = RoutingDictionaryRecord), + (status = 500, description = "Internal server error"), + (status = 400, description = "Malformed request"), + (status = 403, description = "Malformed request"), + (status = 422, description = "Unprocessable request") + ), + tag = "Routing", + operation_id = "Deactivate a routing config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_unlink_config() {} + +/// Routing - Update Default Config +/// +/// Update default fallback config +#[utoipa::path( + post, + path = "/routing/default", + request_body = Vec, + responses( + (status = 200, description = "Successfully updated default config", body = Vec), + (status = 500, description = "Internal server error"), + (status = 400, description = "Malformed request"), + (status = 422, description = "Unprocessable request") + ), + tag = "Routing", + operation_id = "Update default fallback config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_update_default_config() {} + +/// Routing - Retrieve Default Config +/// +/// Retrieve default fallback config +#[utoipa::path( + get, + path = "/routing/default", + responses( + (status = 200, description = "Successfully retrieved default config", body = Vec), + (status = 500, description = "Internal server error") + ), + tag = "Routing", + operation_id = "Retrieve default fallback config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_default_config() {} + +/// Routing - Retrieve Config +/// +/// Retrieve active config +#[utoipa::path( + get, + path = "/routing/active", + params( + ("profile_id" = Option, Query, description = "The unique identifier for a merchant profile"), + ), + responses( + (status = 200, description = "Successfully retrieved active config", body = LinkedRoutingConfigRetrieveResponse), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 403, description = "Forbidden") + ), + tag = "Routing", + operation_id = "Retrieve active config", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_linked_config() {} + +/// Routing - Retrieve Default For Profile +/// +/// Retrieve default config for profiles +#[utoipa::path( + get, + path = "/routing/default/profile", + responses( + (status = 200, description = "Successfully retrieved default config", body = ProfileDefaultRoutingConfig), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing") + ), + tag = "Routing", + operation_id = "Retrieve default configs for all profiles", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_retrieve_default_config_for_profiles() {} + +/// Routing - Update Default For Profile +/// +/// Update default config for profiles +#[utoipa::path( + post, + path = "/routing/default/profile/{profile_id}", + request_body = Vec, + params( + ("profile_id" = String, Path, description = "The unique identifier for a profile"), + ), + responses( + (status = 200, description = "Successfully updated default config for profile", body = ProfileDefaultRoutingConfig), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 400, description = "Malformed request"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Update default configs for all profiles", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn routing_update_default_config_for_profile() {} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 2c3ef2911c22..e575daf7e7ad 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -21,7 +21,7 @@ olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytic oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] -openapi = ["olap", "oltp", "payouts"] +openapi = ["olap", "oltp", "payouts", "dep:openapi"] vergen = ["router_env/vergen"] backwards_compatibility = ["api_models/backwards_compatibility"] business_profile_routing = ["api_models/business_profile_routing"] @@ -96,7 +96,6 @@ tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } unicode-segmentation = "1.10.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } -utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } validator = "0.16.0" x509-parser = "0.15.0" @@ -121,6 +120,7 @@ router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } +openapi = { version = "0.1.0", path = "../openapi", optional = true } erased-serde = "0.3.31" quick-xml = { version = "0.31.0", features = ["serialize"] } rdkafka = "0.36.0" @@ -139,6 +139,7 @@ time = { version = "0.3.21", features = ["macros"] } tokio = "1.35.1" wiremock = "0.5.18" + # First party dev-dependencies test_utils = { version = "0.1.0", path = "../test_utils" } diff --git a/crates/router/src/bin/router.rs b/crates/router/src/bin/router.rs index a02758d8edd5..094f799cb27b 100644 --- a/crates/router/src/bin/router.rs +++ b/crates/router/src/bin/router.rs @@ -9,24 +9,6 @@ async fn main() -> ApplicationResult<()> { // get commandline config before initializing config let cmd_line = ::parse(); - #[cfg(feature = "openapi")] - { - use router::configs::settings::Subcommand; - if let Some(Subcommand::GenerateOpenapiSpec) = cmd_line.subcommand { - let file_path = "openapi/openapi_spec.json"; - #[allow(clippy::expect_used)] - std::fs::write( - file_path, - ::openapi() - .to_pretty_json() - .expect("Failed to serialize OpenAPI specification as JSON"), - ) - .expect("Failed to write OpenAPI specification to file"); - println!("Successfully saved OpenAPI specification file at '{file_path}'"); - return Ok(()); - } - } - #[allow(clippy::expect_used)] let conf = Settings::with_config_path(cmd_line.config_path) .expect("Unable to construct application configuration"); diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index ee22e40190b0..bb56a173da37 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -12,6 +12,7 @@ pub mod cors; pub mod db; pub mod env; pub(crate) mod macros; + pub mod routes; pub mod workflows; @@ -19,7 +20,6 @@ pub mod workflows; pub mod analytics; pub mod events; pub mod middleware; -pub mod openapi; pub mod services; pub mod types; pub mod utils; @@ -93,15 +93,6 @@ pub fn mk_app( > { let mut server_app = get_application_builder(request_body_limit); - #[cfg(feature = "openapi")] - { - use utoipa::OpenApi; - server_app = server_app.service( - utoipa_swagger_ui::SwaggerUi::new("/docs/{_:.*}") - .url("/docs/openapi.json", openapi::ApiDoc::openapi()), - ); - } - #[cfg(feature = "dummy_connector")] { use routes::DummyConnector; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 44822efddc48..f490cee8dab3 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -458,7 +458,7 @@ impl Routing { ) .service( web::resource("") - .route(web::get().to(cloud_routing::routing_retrieve_dictionary)) + .route(web::get().to(cloud_routing::list_routing_configs)) .route(web::post().to(cloud_routing::routing_create_config)), ) .service( diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index e7a0804500c1..012030df6be9 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -8,21 +8,6 @@ use crate::{ types::api::customers, }; -/// Create Customer -/// -/// Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details. -#[utoipa::path( - post, - path = "/customers", - request_body = CustomerRequest, - responses( - (status = 200, description = "Customer Created", body = CustomerResponse), - (status = 400, description = "Invalid data") - ), - tag = "Customers", - operation_id = "Create a Customer", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomersCreate))] pub async fn customers_create( state: web::Data, @@ -45,21 +30,7 @@ pub async fn customers_create( )) .await } -/// Retrieve Customer -/// -/// Retrieve a customer's details. -#[utoipa::path( - get, - path = "/customers/{customer_id}", - params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), - responses( - (status = 200, description = "Customer Retrieved", body = CustomerResponse), - (status = 404, description = "Customer was not found") - ), - tag = "Customers", - operation_id = "Retrieve a Customer", - security(("api_key" = []), ("ephemeral_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::CustomersRetrieve))] pub async fn customers_retrieve( state: web::Data, @@ -93,20 +64,6 @@ pub async fn customers_retrieve( .await } -/// List customers for a merchant -/// -/// To filter and list the customers for a particular merchant id -#[utoipa::path( - post, - path = "/customers/list", - responses( - (status = 200, description = "Customers retrieved", body = Vec), - (status = 400, description = "Invalid Data"), - ), - tag = "Customers List", - operation_id = "List all Customers for a Merchant", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomersList))] pub async fn customers_list(state: web::Data, req: HttpRequest) -> HttpResponse { let flow = Flow::CustomersList; @@ -127,22 +84,6 @@ pub async fn customers_list(state: web::Data, req: HttpRequest) -> Htt .await } -/// Update Customer -/// -/// Updates the customer's details in a customer object. -#[utoipa::path( - post, - path = "/customers/{customer_id}", - request_body = CustomerRequest, - params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), - responses( - (status = 200, description = "Customer was Updated", body = CustomerResponse), - (status = 404, description = "Customer was not found") - ), - tag = "Customers", - operation_id = "Update a Customer", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::CustomersUpdate))] pub async fn customers_update( state: web::Data, @@ -168,21 +109,7 @@ pub async fn customers_update( )) .await } -/// Delete Customer -/// -/// Delete a customer record. -#[utoipa::path( - delete, - path = "/customers/{customer_id}", - params (("customer_id" = String, Path, description = "The unique identifier for the Customer")), - responses( - (status = 200, description = "Customer was Deleted", body = CustomerDeleteResponse), - (status = 404, description = "Customer was not found") - ), - tag = "Customers", - operation_id = "Delete a Customer", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::CustomersDelete))] pub async fn customers_delete( state: web::Data, diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index 342fe23c38ae..eafd61dcded8 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -10,7 +10,7 @@ use crate::{ /// Mandates - Retrieve Mandate /// -/// Retrieve a mandate +/// Retrieves a mandate created using the Payments/Create API #[utoipa::path( get, path = "/mandates/{mandate_id}", @@ -49,12 +49,12 @@ pub async fn get_mandate( } /// Mandates - Revoke Mandate /// -/// Revoke a mandate +/// Revokes a mandate created using the Payments/Create API #[utoipa::path( post, path = "/mandates/revoke/{mandate_id}", params( - ("mandate_id" = String, Path, description = "The identifier for mandate") + ("mandate_id" = String, Path, description = "The identifier for a mandate") ), responses( (status = 200, description = "The mandate was revoked successfully", body = MandateRevokedResponse), diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 55564a6386f4..ef7047bffabc 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -16,21 +16,6 @@ use crate::{ utils::Encode, }; -/// PaymentMethods - Create -/// -/// To create a payment method against a customer object. In case of cards, this API could be used only by PCI compliant merchants -#[utoipa::path( - post, - path = "/payment_methods", - request_body = PaymentMethodCreate, - responses( - (status = 200, description = "Payment Method Created", body = PaymentMethodResponse), - (status = 400, description = "Invalid Data") - ), - tag = "Payment Methods", - operation_id = "Create a Payment Method", - security(("api_key" = [])) -)] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] pub async fn create_payment_method_api( state: web::Data, @@ -57,30 +42,7 @@ pub async fn create_payment_method_api( )) .await } -/// List payment methods for a Merchant -/// -/// To filter and list the applicable payment methods for a particular Merchant ID -#[utoipa::path( - get, - path = "/account/payment_methods", - params ( - ("account_id" = String, Path, description = "The unique identifier for the merchant account"), - ("accepted_country" = Vec, Query, description = "The two-letter ISO currency code"), - ("accepted_currency" = Vec, Path, description = "The three-letter ISO currency code"), - ("minimum_amount" = i64, Query, description = "The minimum amount accepted for processing by the particular payment method."), - ("maximum_amount" = i64, Query, description = "The maximum amount amount accepted for processing by the particular payment method."), - ("recurring_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for recurring payments"), - ("installment_payment_enabled" = bool, Query, description = "Indicates whether the payment method is eligible for installment payments"), - ), - responses( - (status = 200, description = "Payment Methods retrieved", body = PaymentMethodListResponse), - (status = 400, description = "Invalid Data"), - (status = 404, description = "Payment Methods does not exist in records") - ), - tag = "Payment Methods", - operation_id = "List all Payment Methods for a Merchant", - security(("api_key" = []), ("publishable_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsList))] pub async fn list_payment_method_api( state: web::Data, @@ -214,23 +176,7 @@ pub async fn list_customer_payment_method_api_client( )) .await } -/// Payment Method - Retrieve -/// -/// To retrieve a payment method -#[utoipa::path( - get, - path = "/payment_methods/{method_id}", - params ( - ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), - ), - responses( - (status = 200, description = "Payment Method retrieved", body = PaymentMethodResponse), - (status = 404, description = "Payment Method does not exist in records") - ), - tag = "Payment Methods", - operation_id = "Retrieve a Payment method", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsRetrieve))] pub async fn payment_method_retrieve_api( state: web::Data, @@ -254,24 +200,7 @@ pub async fn payment_method_retrieve_api( )) .await } -/// Payment Method - Update -/// -/// To update an existing payment method attached to a customer object. This API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments -#[utoipa::path( - post, - path = "/payment_methods/{method_id}", - params ( - ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), - ), - request_body = PaymentMethodUpdate, - responses( - (status = 200, description = "Payment Method updated", body = PaymentMethodResponse), - (status = 404, description = "Payment Method does not exist in records") - ), - tag = "Payment Methods", - operation_id = "Update a Payment method", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsUpdate))] pub async fn payment_method_update_api( state: web::Data, @@ -301,23 +230,7 @@ pub async fn payment_method_update_api( )) .await } -/// Payment Method - Delete -/// -/// Delete payment method -#[utoipa::path( - delete, - path = "/payment_methods/{method_id}", - params ( - ("method_id" = String, Path, description = "The unique identifier for the Payment Method"), - ), - responses( - (status = 200, description = "Payment Method deleted", body = PaymentMethodDeleteResponse), - (status = 404, description = "Payment Method does not exist in records") - ), - tag = "Payment Methods", - operation_id = "Delete a Payment method", - security(("api_key" = [])) -)] + #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsDelete))] pub async fn payment_method_delete_api( state: web::Data, diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 379cd4f2f1fc..b822e6d4e686 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -17,12 +17,6 @@ use crate::{ payments::{self, PaymentRedirectFlow}, utils as core_utils, }, - // openapi::examples::{ - // PAYMENTS_CREATE, PAYMENTS_CREATE_MINIMUM_FIELDS, PAYMENTS_CREATE_WITH_ADDRESS, - // PAYMENTS_CREATE_WITH_CUSTOMER_DATA, PAYMENTS_CREATE_WITH_FORCED_3DS, - // PAYMENTS_CREATE_WITH_MANUAL_CAPTURE, PAYMENTS_CREATE_WITH_NOON_ORDER_CATETORY, - // PAYMENTS_CREATE_WITH_ORDER_DETAILS, - // }, routes::lock_utils, services::{api, authentication as auth}, types::{ @@ -1153,7 +1147,7 @@ where ("payment_id" = String, Path, description = "The identifier for payment") ), responses( - (status = 200, description = "Payment authorized amount incremented"), + (status = 200, description = "Payment authorized amount incremented", body = PaymentsResponse), (status = 400, description = "Missing mandatory fields") ), tag = "Payments", diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index e7e31cb36aeb..0f139e936146 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -113,7 +113,7 @@ pub async fn routing_retrieve_config( #[cfg(feature = "olap")] #[instrument(skip_all)] -pub async fn routing_retrieve_dictionary( +pub async fn list_routing_configs( state: web::Data, req: HttpRequest, #[cfg(feature = "business_profile_routing")] query: web::Query, diff --git a/crates/router_derive/src/lib.rs b/crates/router_derive/src/lib.rs index 109003e0cc41..c4b6bf3f470e 100644 --- a/crates/router_derive/src/lib.rs +++ b/crates/router_derive/src/lib.rs @@ -505,7 +505,10 @@ pub fn operation_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// } /// ``` -#[proc_macro_derive(PolymorphicSchema, attributes(mandatory_in, generate_schemas))] +#[proc_macro_derive( + PolymorphicSchema, + attributes(mandatory_in, generate_schemas, remove_in) +)] pub fn polymorphic_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); diff --git a/crates/router_derive/src/macros/generate_schema.rs b/crates/router_derive/src/macros/generate_schema.rs index 05d5b2919e11..4dd6db5d60df 100644 --- a/crates/router_derive/src/macros/generate_schema.rs +++ b/crates/router_derive/src/macros/generate_schema.rs @@ -1,7 +1,7 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use indexmap::IndexMap; -use syn::{self, parse_quote, punctuated::Punctuated, Token}; +use syn::{self, parse::Parse, parse_quote, punctuated::Punctuated, Token}; use crate::macros::helpers; @@ -19,6 +19,74 @@ fn get_inner_path_ident(attribute: &syn::Attribute) -> syn::Result>()) } +#[allow(dead_code)] +/// Get the type of field +fn get_field_type(field_type: syn::Type) -> syn::Result { + if let syn::Type::Path(path) = field_type { + path.path + .segments + .last() + .map(|last_path_segment| last_path_segment.ident.to_owned()) + .ok_or(syn::Error::new( + proc_macro2::Span::call_site(), + "Atleast one ident must be specified", + )) + } else { + Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Only path fields are supported", + )) + } +} + +#[allow(dead_code)] +/// Get the inner type of option +fn get_inner_option_type(field: &syn::Type) -> syn::Result { + if let syn::Type::Path(ref path) = &field { + if let Some(segment) = path.path.segments.last() { + if let syn::PathArguments::AngleBracketed(ref args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(ty)) = args.args.first() { + return get_field_type(ty.clone()); + } + } + } + } + + Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Only path fields are supported", + )) +} + +mod schema_keyword { + use syn::custom_keyword; + + custom_keyword!(schema); +} + +#[derive(Debug, Clone)] +pub struct SchemaMeta { + struct_name: syn::Ident, + type_ident: syn::Ident, +} + +/// parse #[mandatory_in(PaymentsCreateRequest = u64)] +impl Parse for SchemaMeta { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let struct_name = input.parse::()?; + input.parse::()?; + let type_ident = input.parse::()?; + Ok(Self { + struct_name, + type_ident, + }) + } +} + +impl quote::ToTokens for SchemaMeta { + fn to_tokens(&self, _: &mut proc_macro2::TokenStream) {} +} + pub fn polymorphic_macro_derive_inner( input: syn::DeriveInput, ) -> syn::Result { @@ -30,12 +98,21 @@ pub fn polymorphic_macro_derive_inner( // Go through all the fields and create a mapping of required fields for a schema // PaymentsCreate -> ["amount","currency"] - // This will be stored in a hashset + // This will be stored in the hashmap with key as // required_fields -> ((amount, PaymentsCreate), (currency, PaymentsCreate)) - let mut required_fields = HashSet::<(syn::Ident, syn::Ident)>::new(); + // and values as the type + // + // (amount, PaymentsCreate) -> Amount + let mut required_fields = HashMap::<(syn::Ident, syn::Ident), syn::Ident>::new(); + + // These fields will be removed in the schema + // PaymentsUpdate -> ["client_secret"] + // This will be stored in a hashset + // hide_fields -> ((client_secret, PaymentsUpdate)) + let mut hide_fields = HashSet::<(syn::Ident, syn::Ident)>::new(); let mut all_fields = IndexMap::>::new(); - fields.iter().for_each(|field| { + for field in fields { // Partition the attributes of a field into two vectors // One with #[mandatory_in] attributes present // Rest of the attributes ( include only the schema attribute, serde is not required) @@ -44,6 +121,12 @@ pub fn polymorphic_macro_derive_inner( .iter() .partition::, _>(|attribute| attribute.path().is_ident("mandatory_in")); + let hidden_fields = field + .attrs + .iter() + .filter(|attribute| attribute.path().is_ident("remove_in")) + .collect::>(); + // Other attributes ( schema ) are to be printed as is other_attributes .iter() @@ -68,7 +151,31 @@ pub fn polymorphic_macro_derive_inner( // ("currency", PaymentsConfirmRequest) // // For these attributes, we need to later add #[schema(required = true)] attribute - _ = mandatory_attribute + let field_ident = field.ident.ok_or(syn::Error::new( + proc_macro2::Span::call_site(), + "Cannot use `mandatory_in` on unnamed fields", + ))?; + + // Parse the #[mandatory_in(PaymentsCreateRequest = u64)] and insert into hashmap + // key -> ("amount", PaymentsCreateRequest) + // value -> u64 + if let Some(mandatory_in_attribute) = + helpers::get_metadata_inner::("mandatory_in", mandatory_attribute)?.first() + { + let key = ( + field_ident.clone(), + mandatory_in_attribute.struct_name.clone(), + ); + let value = mandatory_in_attribute.type_ident.clone(); + required_fields.insert(key, value); + } + + // Hidden fields are to be inserted in the Hashset + // The hashset will store it in this format + // ("client_secret", PaymentsUpdate) + // + // These fields will not be added to the struct + _ = hidden_fields .iter() // Filter only #[mandatory_in] attributes .map(|&attribute| get_inner_path_ident(attribute)) @@ -76,40 +183,59 @@ pub fn polymorphic_macro_derive_inner( let res = schemas .map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))? .iter() - .filter_map(|schema| field.ident.to_owned().zip(Some(schema.to_owned()))) + .map(|schema| (field_ident.clone(), schema.to_owned())) .collect::>(); - required_fields.extend(res); + hide_fields.extend(res); Ok::<_, syn::Error>(()) }); - }); + } + // iterate over the schemas and build them with their fields let schemas = schemas_to_create .iter() .map(|schema| { let fields = all_fields .iter() - .flat_map(|(field, value)| { - let mut attributes = value - .iter() - .map(|attribute| quote::quote!(#attribute)) - .collect::>(); - - // If the field is required for this schema, then add - // #[schema(required = true)] for this field - let required_attribute: syn::Attribute = - parse_quote!(#[schema(required = true)]); - - // Can be none, because tuple fields have no ident - field.ident.to_owned().and_then(|field_ident| { - required_fields - .contains(&(field_ident, schema.to_owned())) - .then(|| attributes.push(quote::quote!(#required_attribute))) - }); - - quote::quote! { - #(#attributes)* - #field, + .filter_map(|(field, attributes)| { + let mut final_attributes = attributes.clone(); + + if let Some(field_ident) = field.ident.to_owned() { + // If the field is required for this schema, then add + // #[schema(value_type = type)] for this field + if let Some(required_field_type) = + required_fields.get(&(field_ident.clone(), schema.to_owned())) + { + // This is a required field in the Schema + // Add the value type and remove original value type ( if present ) + let attribute_without_schema_type = attributes + .iter() + .filter(|attribute| !attribute.path().is_ident("schema")) + .map(Clone::clone) + .collect::>(); + + final_attributes = attribute_without_schema_type; + + let value_type_attribute: syn::Attribute = + parse_quote!(#[schema(value_type = #required_field_type)]); + final_attributes.push(value_type_attribute); + } + } + + // If the field is to be not shown then + let is_hidden_field = field + .ident + .clone() + .map(|field_ident| hide_fields.contains(&(field_ident, schema.to_owned()))) + .unwrap_or(false); + + if is_hidden_field { + None + } else { + Some(quote::quote! { + #(#final_attributes)* + #field, + }) } }) .collect::>(); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 135ca638109e..7858029961a8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11,7 +11,7 @@ "license": { "name": "Apache-2.0" }, - "version": "0.2.0" + "version": "0.1.0" }, "servers": [ { @@ -26,7 +26,7 @@ "Payment Methods" ], "summary": "List payment methods for a Merchant", - "description": "List payment methods for a Merchant\n\nTo filter and list the applicable payment methods for a particular Merchant ID", + "description": "List payment methods for a Merchant\n\nLists the applicable payment methods for a particular Merchant ID.\nUse the client secret and publishable key authorization to list all relevant payment methods of the merchant for the payment corresponding to the client secret.", "operationId": "List all Payment Methods for a Merchant", "parameters": [ { @@ -75,7 +75,7 @@ { "name": "maximum_amount", "in": "query", - "description": "The maximum amount amount accepted for processing by the particular payment method.", + "description": "The maximum amount accepted for processing by the particular payment method.", "required": true, "schema": { "type": "integer", @@ -129,19 +129,19 @@ ] } }, - "/accounts/{account_id}/connectors": { + "/account/{account_id}/business_profile": { "get": { "tags": [ - "Merchant Connector Account" + "Business Profile" ], - "summary": "Merchant Connector - List", - "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", - "operationId": "List all Merchant Connectors", + "summary": "Business Profile - List", + "description": "Business Profile - List\n\nLists all the *business profiles* under a merchant", + "operationId": "List Business Profiles", "parameters": [ { "name": "account_id", "in": "path", - "description": "The unique identifier for the merchant account", + "description": "Merchant Identifier", "required": true, "schema": { "type": "string" @@ -150,43 +150,58 @@ ], "responses": { "200": { - "description": "Merchant Connector list retrieved successfully", + "description": "Business profiles Retrieved", "content": { "application/json": { "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/MerchantConnectorResponse" + "$ref": "#/components/schemas/BusinessProfileResponse" } } } } - }, - "401": { - "description": "Unauthorized request" - }, - "404": { - "description": "Merchant Connector does not exist in records" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] }, "post": { "tags": [ - "Merchant Connector Account" + "Business Profile" + ], + "summary": "Business Profile - Create", + "description": "Business Profile - Create\n\nCreates a new *business profile* for a merchant", + "operationId": "Create A Business Profile", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } ], - "summary": "Merchant Connector - Create", - "description": "Merchant Connector - Create\n\nCreate a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", - "operationId": "Create a Merchant Connector", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MerchantConnectorCreate" + "$ref": "#/components/schemas/BusinessProfileCreate" + }, + "examples": { + "Create a business profile with minimal fields": { + "value": {} + }, + "Create a business profile with profile name": { + "value": { + "profile_name": "shoe_business" + } + } } } }, @@ -194,17 +209,17 @@ }, "responses": { "200": { - "description": "Merchant Connector Created", + "description": "Business Account Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MerchantConnectorResponse" + "$ref": "#/components/schemas/BusinessProfileResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid data" } }, "security": [ @@ -214,14 +229,14 @@ ] } }, - "/accounts/{account_id}/connectors/{connector_id}": { + "/account/{account_id}/business_profile/{profile_id}": { "get": { "tags": [ - "Merchant Connector Account" + "Business Profile" ], - "summary": "Merchant Connector - Retrieve", - "description": "Merchant Connector - Retrieve\n\nRetrieve Merchant Connector Details", - "operationId": "Retrieve a Merchant Connector", + "summary": "Business Profile - Retrieve", + "description": "Business Profile - Retrieve\n\nRetrieve existing *business profile*", + "operationId": "Retrieve a Business Profile", "parameters": [ { "name": "account_id", @@ -233,47 +248,43 @@ } }, { - "name": "connector_id", + "name": "profile_id", "in": "path", - "description": "The unique identifier for the Merchant Connector", + "description": "The unique identifier for the business profile", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], "responses": { "200": { - "description": "Merchant Connector retrieved successfully", + "description": "Business Profile Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MerchantConnectorResponse" + "$ref": "#/components/schemas/BusinessProfileResponse" } } } }, - "401": { - "description": "Unauthorized request" - }, - "404": { - "description": "Merchant Connector does not exist in records" + "400": { + "description": "Invalid data" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] }, "post": { "tags": [ - "Merchant Connector Account" + "Business Profile" ], - "summary": "Merchant Connector - Update", - "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc.", - "operationId": "Update a Merchant Connector", + "summary": "Business Profile - Update", + "description": "Business Profile - Update\n\nUpdate the *business profile*", + "operationId": "Update a Business Profile", "parameters": [ { "name": "account_id", @@ -285,13 +296,12 @@ } }, { - "name": "connector_id", + "name": "profile_id", "in": "path", - "description": "The unique identifier for the Merchant Connector", + "description": "The unique identifier for the business profile", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -299,7 +309,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MerchantConnectorUpdate" + "$ref": "#/components/schemas/BusinessProfileCreate" + }, + "examples": { + "Update business profile with profile name fields": { + "value": { + "profile_name": "shoe_business" + } + } } } }, @@ -307,35 +324,32 @@ }, "responses": { "200": { - "description": "Merchant Connector Updated", + "description": "Business Profile Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MerchantConnectorResponse" + "$ref": "#/components/schemas/BusinessProfileResponse" } } } }, - "401": { - "description": "Unauthorized request" - }, - "404": { - "description": "Merchant Connector does not exist in records" + "400": { + "description": "Invalid data" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] }, "delete": { "tags": [ - "Merchant Connector Account" + "Business Profile" ], - "summary": "Merchant Connector - Delete", - "description": "Merchant Connector - Delete\n\nDelete or Detach a Merchant Connector from Merchant Account", - "operationId": "Delete a Merchant Connector", + "summary": "Business Profile - Delete", + "description": "Business Profile - Delete\n\nDelete the *business profile*", + "operationId": "Delete the Business Profile", "parameters": [ { "name": "account_id", @@ -347,71 +361,28 @@ } }, { - "name": "connector_id", + "name": "profile_id", "in": "path", - "description": "The unique identifier for the Merchant Connector", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "Merchant Connector Deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" - } - } - } - }, - "401": { - "description": "Unauthorized request" - }, - "404": { - "description": "Merchant Connector does not exist in records" - } - }, - "security": [ - { - "admin_api_key": [] - } - ] - } - }, - "/blocklist": { - "get": { - "tags": [ - "Blocklist" - ], - "operationId": "List Blocked fingerprints of a particular kind", - "parameters": [ - { - "name": "data_kind", - "in": "query", - "description": "Kind of the fingerprint list requested", + "description": "The unique identifier for the business profile", "required": true, "schema": { - "$ref": "#/components/schemas/BlocklistDataKind" + "type": "string" } } ], "responses": { "200": { - "description": "Blocked Fingerprints", + "description": "Business profiles Deleted", "content": { - "application/json": { + "text/plain": { "schema": { - "$ref": "#/components/schemas/BlocklistResponse" + "type": "boolean" } } } }, "400": { - "description": "Invalid Data" + "description": "Invalid data" } }, "security": [ @@ -419,17 +390,42 @@ "api_key": [] } ] - }, + } + }, + "/accounts": { "post": { "tags": [ - "Blocklist" + "Merchant Account" ], - "operationId": "Block a Fingerprint", + "summary": "Merchant Account - Create", + "description": "Merchant Account - Create\n\nCreate a new account for a *merchant* and the *merchant* could be a seller or retailer or client who likes to receive and send payments.", + "operationId": "Create a Merchant Account", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BlocklistRequest" + "$ref": "#/components/schemas/MerchantAccountCreate" + }, + "examples": { + "Create a merchant account with minimal fields": { + "value": { + "merchant_id": "merchant_abc" + } + }, + "Create a merchant account with return url": { + "value": { + "merchant_id": "merchant_abc", + "return_url": "https://example.com" + } + }, + "Create a merchant account with webhook url": { + "value": { + "merchant_id": "merchant_abc", + "webhook_details": { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + } + } } } }, @@ -437,75 +433,111 @@ }, "responses": { "200": { - "description": "Fingerprint Blocked", + "description": "Merchant Account Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BlocklistResponse" + "$ref": "#/components/schemas/MerchantAccountResponse" } } } }, "400": { - "description": "Invalid Data" + "description": "Invalid data" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - }, - "delete": { + } + }, + "/accounts/{account_id}": { + "get": { "tags": [ - "Blocklist" + "Merchant Account" ], - "operationId": "Unblock a Fingerprint", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BlocklistRequest" - } + "summary": "Merchant Account - Retrieve", + "description": "Merchant Account - Retrieve\n\nRetrieve a *merchant* account details.", + "operationId": "Retrieve a Merchant Account", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Fingerprint Unblocked", + "description": "Merchant Account Retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BlocklistResponse" + "$ref": "#/components/schemas/MerchantAccountResponse" } } } }, - "400": { - "description": "Invalid Data" + "404": { + "description": "Merchant account not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - } - }, - "/customers": { + }, "post": { "tags": [ - "Customers" + "Merchant Account" + ], + "summary": "Merchant Account - Update", + "description": "Merchant Account - Update\n\nUpdates details of an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc", + "operationId": "Update a Merchant Account", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } ], - "summary": "Create Customer", - "description": "Create Customer\n\nCreate a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details.", - "operationId": "Create a Customer", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerRequest" + "$ref": "#/components/schemas/MerchantAccountUpdate" + }, + "examples": { + "Update merchant name": { + "value": { + "merchant_id": "merchant_abc", + "merchant_name": "merchant_name" + } + }, + "Update return url": { + "value": { + "merchant_id": "merchant_abc", + "return_url": "https://example.com" + } + }, + "Update webhook url": { + "value": { + "merchant_id": "merchant_abc", + "webhook_details": { + "webhook_url": "https://webhook.site/a5c54f75-1f7e-4545-b781-af525b7e37a0" + } + } + } } } }, @@ -513,231 +545,292 @@ }, "responses": { "200": { - "description": "Customer Created", + "description": "Merchant Account Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerResponse" + "$ref": "#/components/schemas/MerchantAccountResponse" } } } }, - "400": { - "description": "Invalid data" + "404": { + "description": "Merchant account not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] - } - }, - "/customers/list": { - "post": { + }, + "delete": { "tags": [ - "Customers List" + "Merchant Account" + ], + "summary": "Merchant Account - Delete", + "description": "Merchant Account - Delete\n\nDelete a *merchant* account", + "operationId": "Delete a Merchant Account", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } ], - "summary": "List customers for a merchant", - "description": "List customers for a merchant\n\nTo filter and list the customers for a particular merchant id", - "operationId": "List all Customers for a Merchant", "responses": { "200": { - "description": "Customers retrieved", + "description": "Merchant Account Deleted", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerResponse" - } + "$ref": "#/components/schemas/MerchantAccountDeleteResponse" } } } }, - "400": { - "description": "Invalid Data" + "404": { + "description": "Merchant account not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/customers/payment_methods": { + "/accounts/{account_id}/connectors": { "get": { "tags": [ - "Payment Methods" + "Merchant Connector Account" ], - "summary": "List payment methods for a Customer", - "description": "List payment methods for a Customer\n\nTo filter and list the applicable payment methods for a particular Customer ID", - "operationId": "List all Payment Methods for a Customer", + "summary": "Merchant Connector - List", + "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", + "operationId": "List all Merchant Connectors", "parameters": [ { - "name": "client-secret", + "name": "account_id", "in": "path", - "description": "A secret known only to your application and the authorization server", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } - }, - { - "name": "accepted_country", - "in": "query", - "description": "The two-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "accepted_currency", - "in": "path", - "description": "The three-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + } + ], + "responses": { + "200": { + "description": "Merchant Connector list retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } } } }, - { - "name": "minimum_amount", - "in": "query", - "description": "The minimum amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "maximum_amount", - "in": "query", - "description": "The maximum amount amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "recurring_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for recurring payments", - "required": true, - "schema": { - "type": "boolean" - } + "401": { + "description": "Unauthorized request" }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ { - "name": "installment_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for installment payments", - "required": true, - "schema": { - "type": "boolean" - } + "admin_api_key": [] } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" ], + "summary": "Merchant Connector - Create", + "description": "Merchant Connector - Create\n\nCreates a new Merchant Connector for the merchant account. The connector could be a payment processor/facilitator/acquirer or a provider of specialized services like Fraud/Accounting etc.", + "operationId": "Create a Merchant Connector", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorCreate" + }, + "examples": { + "Create a merchant account with custom connector label": { + "value": { + "connector_account_details": { + "api_key": "{{adyen-api-key}}", + "auth_type": "BodyKey", + "key1": "{{adyen_merchant_account}}" + }, + "connector_label": "EU_adyen", + "connector_name": "adyen", + "connector_type": "fiz_operations" + } + }, + "Create a merchant connector account under a specific business profile": { + "value": { + "connector_account_details": { + "api_key": "{{adyen-api-key}}", + "auth_type": "BodyKey", + "key1": "{{adyen_merchant_account}}" + }, + "connector_name": "adyen", + "connector_type": "fiz_operations", + "profile_id": "{{profile_id}}" + } + }, + "Create a merchant connector account with minimal fields": { + "value": { + "connector_account_details": { + "api_key": "{{adyen-api-key}}", + "auth_type": "BodyKey", + "key1": "{{adyen_merchant_account}}" + }, + "connector_name": "adyen", + "connector_type": "fiz_operations" + } + } + } + } + }, + "required": true + }, "responses": { "200": { - "description": "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", + "description": "Merchant Connector Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" + "$ref": "#/components/schemas/MerchantConnectorResponse" } } } }, "400": { - "description": "Invalid Data" - }, - "404": { - "description": "Payment Methods does not exist in records" + "description": "Missing Mandatory fields" } }, "security": [ { - "publishable_key": [] + "admin_api_key": [] } ] } }, - "/customers/{customer_id}": { + "/accounts/{account_id}/connectors/{connector_id}": { "get": { "tags": [ - "Customers" + "Merchant Connector Account" ], - "summary": "Retrieve Customer", - "description": "Retrieve Customer\n\nRetrieve a customer's details.", - "operationId": "Retrieve a Customer", + "summary": "Merchant Connector - Retrieve", + "description": "Merchant Connector - Retrieve\n\nRetrieves details of a Connector account", + "operationId": "Retrieve a Merchant Connector", "parameters": [ { - "name": "customer_id", + "name": "account_id", "in": "path", - "description": "The unique identifier for the Customer", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } } ], "responses": { "200": { - "description": "Customer Retrieved", + "description": "Merchant Connector retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerResponse" + "$ref": "#/components/schemas/MerchantConnectorResponse" } } } }, + "401": { + "description": "Unauthorized request" + }, "404": { - "description": "Customer was not found" + "description": "Merchant Connector does not exist in records" } }, "security": [ { - "api_key": [] - }, - { - "ephemeral_key": [] + "admin_api_key": [] } ] }, "post": { "tags": [ - "Customers" + "Merchant Connector Account" ], - "summary": "Update Customer", - "description": "Update Customer\n\nUpdates the customer's details in a customer object.", - "operationId": "Update a Customer", + "summary": "Merchant Connector - Update", + "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector account. Helpful in enabling/disabling different payment methods and other settings for the connector", + "operationId": "Update a Merchant Connector", "parameters": [ { - "name": "customer_id", + "name": "account_id", "in": "path", - "description": "The unique identifier for the Customer", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerRequest" + "$ref": "#/components/schemas/MerchantConnectorUpdate" + }, + "examples": { + "Enable card payment method": { + "value": { + "connector_type": "fiz_operations", + "payment_methods_enabled": [ + { + "payment_method": "card" + } + ] + } + }, + "Update webhook secret": { + "value": { + "connector_webhook_details": { + "merchant_secret": "{{webhook_secret}}" + } + } + } } } }, @@ -745,343 +838,320 @@ }, "responses": { "200": { - "description": "Customer was Updated", + "description": "Merchant Connector Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerResponse" + "$ref": "#/components/schemas/MerchantConnectorResponse" } } } }, + "401": { + "description": "Unauthorized request" + }, "404": { - "description": "Customer was not found" + "description": "Merchant Connector does not exist in records" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] }, "delete": { "tags": [ - "Customers" + "Merchant Connector Account" ], - "summary": "Delete Customer", - "description": "Delete Customer\n\nDelete a customer record.", - "operationId": "Delete a Customer", + "summary": "Merchant Connector - Delete", + "description": "Merchant Connector - Delete\n\nDelete or Detach a Merchant Connector from Merchant Account", + "operationId": "Delete a Merchant Connector", "parameters": [ { - "name": "customer_id", + "name": "account_id", "in": "path", - "description": "The unique identifier for the Customer", + "description": "The unique identifier for the merchant account", "required": true, "schema": { "type": "string" } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } } ], "responses": { "200": { - "description": "Customer was Deleted", + "description": "Merchant Connector Deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerDeleteResponse" + "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" } } } }, + "401": { + "description": "Unauthorized request" + }, "404": { - "description": "Customer was not found" + "description": "Merchant Connector does not exist in records" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/customers/{customer_id}/payment_methods": { - "get": { + "/api_keys/{merchant_id)": { + "post": { "tags": [ - "Payment Methods" + "API Key" ], - "summary": "List payment methods for a Customer", - "description": "List payment methods for a Customer\n\nTo filter and list the applicable payment methods for a particular Customer ID", - "operationId": "List all Payment Methods for a Customer", + "summary": "API Key - Create", + "description": "API Key - Create\n\nCreate a new API Key for accessing our APIs from your servers. The plaintext API Key will be\ndisplayed only once on creation, so ensure you store it securely.", + "operationId": "Create an API Key", "parameters": [ { - "name": "accepted_country", - "in": "query", - "description": "The two-letter ISO currency code", + "name": "merchant_id", + "in": "path", + "description": "The unique identifier for the merchant account", "required": true, "schema": { - "type": "array", - "items": { - "type": "string" + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" } } }, - { - "name": "accepted_currency", - "in": "path", - "description": "The three-letter ISO currency code", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + "required": true + }, + "responses": { + "200": { + "description": "API Key created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyResponse" + } } } }, + "400": { + "description": "Invalid data" + } + }, + "security": [ { - "name": "minimum_amount", - "in": "query", - "description": "The minimum amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "maximum_amount", - "in": "query", - "description": "The maximum amount amount accepted for processing by the particular payment method.", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, + "admin_api_key": [] + } + ] + } + }, + "/api_keys/{merchant_id)/{key_id}": { + "delete": { + "tags": [ + "API Key" + ], + "summary": "API Key - Revoke", + "description": "API Key - Revoke\n\nRevoke the specified API Key. Once revoked, the API Key can no longer be used for\nauthenticating with our APIs.", + "operationId": "Revoke an API Key", + "parameters": [ { - "name": "recurring_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for recurring payments", + "name": "merchant_id", + "in": "path", + "description": "The unique identifier for the merchant account", "required": true, "schema": { - "type": "boolean" + "type": "string" } }, { - "name": "installment_payment_enabled", - "in": "query", - "description": "Indicates whether the payment method is eligible for installment payments", + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", "required": true, "schema": { - "type": "boolean" + "type": "string" } } ], "responses": { "200": { - "description": "Payment Methods retrieved", + "description": "API Key revoked", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" + "$ref": "#/components/schemas/RevokeApiKeyResponse" } } } }, - "400": { - "description": "Invalid Data" - }, "404": { - "description": "Payment Methods does not exist in records" + "description": "API Key not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/disputes/list": { + "/api_keys/{merchant_id}/{key_id}": { "get": { "tags": [ - "Disputes" + "API Key" ], - "summary": "Disputes - List Disputes", - "description": "Disputes - List Disputes", - "operationId": "List Disputes", + "summary": "API Key - Retrieve", + "description": "API Key - Retrieve\n\nRetrieve information about the specified API Key.", + "operationId": "Retrieve an API Key", "parameters": [ { - "name": "limit", - "in": "query", - "description": "The maximum number of Dispute Objects to include in the response", - "required": false, + "name": "merchant_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, "schema": { - "type": "integer", - "format": "int64", - "nullable": true + "type": "string" } }, { - "name": "dispute_status", - "in": "query", - "description": "The status of dispute", - "required": false, + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", + "required": true, "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/DisputeStatus" - } - ], - "nullable": true + "type": "string" } - }, - { - "name": "dispute_stage", - "in": "query", - "description": "The stage of dispute", - "required": false, - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/DisputeStage" + } + ], + "responses": { + "200": { + "description": "API Key retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrieveApiKeyResponse" } - ], - "nullable": true - } - }, - { - "name": "reason", - "in": "query", - "description": "The reason for dispute", - "required": false, - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "connector", - "in": "query", - "description": "The connector linked to dispute", - "required": false, - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "received_time", - "in": "query", - "description": "The time at which dispute is received", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - { - "name": "received_time.lt", - "in": "query", - "description": "Time less than the dispute received time", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true + } } }, + "404": { + "description": "API Key not found" + } + }, + "security": [ { - "name": "received_time.gt", - "in": "query", - "description": "Time greater than the dispute received time", - "required": false, - "schema": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "API Key" + ], + "summary": "API Key - Update", + "description": "API Key - Update\n\nUpdate information for the specified API Key.", + "operationId": "Update an API Key", + "parameters": [ { - "name": "received_time.lte", - "in": "query", - "description": "Time less than or equals to the dispute received time", - "required": false, + "name": "merchant_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, "schema": { - "type": "string", - "format": "date-time", - "nullable": true + "type": "string" } }, { - "name": "received_time.gte", - "in": "query", - "description": "Time greater than or equals to the dispute received time", - "required": false, + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", + "required": true, "schema": { - "type": "string", - "format": "date-time", - "nullable": true + "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "The dispute list was retrieved successfully", + "description": "API Key updated", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponse" - } + "$ref": "#/components/schemas/RetrieveApiKeyResponse" } } } }, - "401": { - "description": "Unauthorized request" + "404": { + "description": "API Key not found" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/disputes/{dispute_id}": { + "/blocklist": { "get": { "tags": [ - "Disputes" + "Blocklist" ], - "summary": "Disputes - Retrieve Dispute", - "description": "Disputes - Retrieve Dispute", - "operationId": "Retrieve a Dispute", + "operationId": "List Blocked fingerprints of a particular kind", "parameters": [ { - "name": "dispute_id", - "in": "path", - "description": "The identifier for dispute", + "name": "data_kind", + "in": "query", + "description": "Kind of the fingerprint list requested", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/BlocklistDataKind" } } ], "responses": { "200": { - "description": "The dispute was retrieved successfully", + "description": "Blocked Fingerprints", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DisputeResponse" + "$ref": "#/components/schemas/BlocklistResponse" } } } }, - "404": { - "description": "Dispute does not exist in our records" + "400": { + "description": "Invalid Data" } }, "security": [ @@ -1089,21 +1159,17 @@ "api_key": [] } ] - } - }, - "/gsm": { + }, "post": { "tags": [ - "Gsm" + "Blocklist" ], - "summary": "Gsm - Create", - "description": "Gsm - Create\n\nTo create a Gsm Rule", - "operationId": "Create Gsm Rule", + "operationId": "Block a Fingerprint", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmCreateRequest" + "$ref": "#/components/schemas/BlocklistRequest" } } }, @@ -1111,39 +1177,35 @@ }, "responses": { "200": { - "description": "Gsm created", + "description": "Fingerprint Blocked", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmResponse" + "$ref": "#/components/schemas/BlocklistResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid Data" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] - } - }, - "/gsm/delete": { - "post": { + }, + "delete": { "tags": [ - "Gsm" + "Blocklist" ], - "summary": "Gsm - Delete", - "description": "Gsm - Delete\n\nTo delete a Gsm Rule", - "operationId": "Delete Gsm Rule", + "operationId": "Unblock a Fingerprint", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmDeleteRequest" + "$ref": "#/components/schemas/BlocklistRequest" } } }, @@ -1151,39 +1213,47 @@ }, "responses": { "200": { - "description": "Gsm deleted", + "description": "Fingerprint Unblocked", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmDeleteResponse" + "$ref": "#/components/schemas/BlocklistResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid Data" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] } }, - "/gsm/get": { + "/customers": { "post": { "tags": [ - "Gsm" + "Customers" ], - "summary": "Gsm - Get", - "description": "Gsm - Get\n\nTo get a Gsm Rule", - "operationId": "Retrieve Gsm Rule", + "summary": "Customers - Create", + "description": "Customers - Create\n\nCreates a customer object and stores the customer details to be reused for future payments.\nIncase the customer already exists in the system, this API will respond with the customer details.", + "operationId": "Create a Customer", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmRetrieveRequest" + "$ref": "#/components/schemas/CustomerRequest" + }, + "examples": { + "Update name and email of a customer": { + "value": { + "email": "guest@example.com", + "name": "John Doe" + } + } } } }, @@ -1191,120 +1261,187 @@ }, "responses": { "200": { - "description": "Gsm retrieved", + "description": "Customer Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmResponse" + "$ref": "#/components/schemas/CustomerResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid data" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] } }, - "/gsm/update": { + "/customers/list": { "post": { "tags": [ - "Gsm" + "Customers List" ], - "summary": "Gsm - Update", - "description": "Gsm - Update\n\nTo update a Gsm Rule", - "operationId": "Update Gsm Rule", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GsmUpdateRequest" - } - } - }, - "required": true - }, + "summary": "Customers - List", + "description": "Customers - List\n\nLists all the customers for a particular merchant id.", + "operationId": "List all Customers for a Merchant", "responses": { "200": { - "description": "Gsm updated", + "description": "Customers retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GsmResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerResponse" + } } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid Data" } }, "security": [ { - "admin_api_key": [] + "api_key": [] } ] } }, - "/mandates/revoke/{mandate_id}": { - "post": { + "/customers/payment_methods": { + "get": { "tags": [ - "Mandates" + "Payment Methods" ], - "summary": "Mandates - Revoke Mandate", - "description": "Mandates - Revoke Mandate\n\nRevoke a mandate", - "operationId": "Revoke a Mandate", + "summary": "List payment methods for a Payment", + "description": "List payment methods for a Payment\n\nLists all the applicable payment methods for a particular payment tied to the `client_secret`.", + "operationId": "List all Payment Methods for a Customer", "parameters": [ { - "name": "mandate_id", + "name": "client-secret", "in": "path", - "description": "The identifier for mandate", + "description": "A secret known only to your client and the authorization server. Used for client side authentication", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "customer_id", + "in": "path", + "description": "The unique identifier for the customer account", "required": true, "schema": { "type": "string" } + }, + { + "name": "accepted_country", + "in": "query", + "description": "The two-letter ISO currency code", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "accepted_currency", + "in": "path", + "description": "The three-letter ISO currency code", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } + } + }, + { + "name": "minimum_amount", + "in": "query", + "description": "The minimum amount accepted for processing by the particular payment method.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "maximum_amount", + "in": "query", + "description": "The maximum amount accepted for processing by the particular payment method.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "recurring_payment_enabled", + "in": "query", + "description": "Indicates whether the payment method is eligible for recurring payments", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "installment_payment_enabled", + "in": "query", + "description": "Indicates whether the payment method is eligible for installment payments", + "required": true, + "schema": { + "type": "boolean" + } } ], "responses": { "200": { - "description": "The mandate was revoked successfully", + "description": "Payment Methods retrieved for customer tied to its respective client-secret passed in the param", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MandateRevokedResponse" + "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" } } } }, "400": { - "description": "Mandate does not exist in our records" + "description": "Invalid Data" + }, + "404": { + "description": "Payment Methods does not exist in records" } }, "security": [ { - "api_key": [] + "publishable_key": [] } ] } }, - "/mandates/{mandate_id}": { + "/customers/{customer_id}": { "get": { "tags": [ - "Mandates" + "Customers" ], - "summary": "Mandates - Retrieve Mandate", - "description": "Mandates - Retrieve Mandate\n\nRetrieve a mandate", - "operationId": "Retrieve a Mandate", + "summary": "Customers - Retrieve", + "description": "Customers - Retrieve\n\nRetrieves a customer's details.", + "operationId": "Retrieve a Customer", "parameters": [ { - "name": "mandate_id", + "name": "customer_id", "in": "path", - "description": "The identifier for mandate", + "description": "The unique identifier for the Customer", "required": true, "schema": { "type": "string" @@ -1313,39 +1450,40 @@ ], "responses": { "200": { - "description": "The mandate was retrieved successfully", + "description": "Customer Retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MandateResponse" + "$ref": "#/components/schemas/CustomerResponse" } } } }, "404": { - "description": "Mandate does not exist in our records" + "description": "Customer was not found" } }, "security": [ { "api_key": [] + }, + { + "ephemeral_key": [] } ] - } - }, - "/payment_link/{payment_link_id}": { - "get": { + }, + "post": { "tags": [ - "Payments" + "Customers" ], - "summary": "Payments Link - Retrieve", - "description": "Payments Link - Retrieve\n\nTo retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", - "operationId": "Retrieve a Payment Link", + "summary": "Customers - Update", + "description": "Customers - Update\n\nUpdates the customer's details in a customer object.", + "operationId": "Update a Customer", "parameters": [ { - "name": "payment_link_id", + "name": "customer_id", "in": "path", - "description": "The identifier for payment link", + "description": "The unique identifier for the Customer", "required": true, "schema": { "type": "string" @@ -1356,7 +1494,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RetrievePaymentLinkRequest" + "$ref": "#/components/schemas/CustomerRequest" + }, + "examples": { + "Update name and email of a customer": { + "value": { + "email": "guest@example.com", + "name": "John Doe" + } + } } } }, @@ -1364,60 +1510,56 @@ }, "responses": { "200": { - "description": "Gets details regarding payment link", + "description": "Customer was Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RetrievePaymentLinkResponse" + "$ref": "#/components/schemas/CustomerResponse" } } } }, "404": { - "description": "No payment link found" + "description": "Customer was not found" } }, "security": [ { "api_key": [] - }, - { - "publishable_key": [] } ] - } - }, - "/payment_methods": { - "post": { + }, + "delete": { "tags": [ - "Payment Methods" + "Customers" ], - "summary": "PaymentMethods - Create", - "description": "PaymentMethods - Create\n\nTo create a payment method against a customer object. In case of cards, this API could be used only by PCI compliant merchants", - "operationId": "Create a Payment Method", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentMethodCreate" - } + "summary": "Customers - Delete", + "description": "Customers - Delete\n\nDelete a customer record.", + "operationId": "Delete a Customer", + "parameters": [ + { + "name": "customer_id", + "in": "path", + "description": "The unique identifier for the Customer", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Payment Method Created", + "description": "Customer was Deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodResponse" + "$ref": "#/components/schemas/CustomerDeleteResponse" } } } }, - "400": { - "description": "Invalid Data" + "404": { + "description": "Customer was not found" } }, "security": [ @@ -1427,166 +1569,103 @@ ] } }, - "/payment_methods/{method_id}": { + "/customers/{customer_id}/payment_methods": { "get": { "tags": [ "Payment Methods" ], - "summary": "Payment Method - Retrieve", - "description": "Payment Method - Retrieve\n\nTo retrieve a payment method", - "operationId": "Retrieve a Payment method", - "parameters": [ + "summary": "List payment methods for a Customer", + "description": "List payment methods for a Customer\n\nLists all the applicable payment methods for a particular Customer ID.", + "operationId": "List all Payment Methods for a Customer", + "parameters": [ { - "name": "method_id", + "name": "customer_id", "in": "path", - "description": "The unique identifier for the Payment Method", + "description": "The unique identifier for the customer account", "required": true, "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Payment Method retrieved", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentMethodResponse" - } + }, + { + "name": "accepted_country", + "in": "query", + "description": "The two-letter ISO currency code", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" } } }, - "404": { - "description": "Payment Method does not exist in records" - } - }, - "security": [ - { - "api_key": [] - } - ] - }, - "post": { - "tags": [ - "Payment Methods" - ], - "summary": "Payment Method - Update", - "description": "Payment Method - Update\n\nTo update an existing payment method attached to a customer object. This API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments", - "operationId": "Update a Payment method", - "parameters": [ { - "name": "method_id", + "name": "accepted_currency", "in": "path", - "description": "The unique identifier for the Payment Method", + "description": "The three-letter ISO currency code", "required": true, "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentMethodUpdate" + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" } } }, - "required": true - }, - "responses": { - "200": { - "description": "Payment Method updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentMethodResponse" - } - } + { + "name": "minimum_amount", + "in": "query", + "description": "The minimum amount accepted for processing by the particular payment method.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" } }, - "404": { - "description": "Payment Method does not exist in records" - } - }, - "security": [ - { - "api_key": [] - } - ] - }, - "delete": { - "tags": [ - "Payment Methods" - ], - "summary": "Payment Method - Delete", - "description": "Payment Method - Delete\n\nDelete payment method", - "operationId": "Delete a Payment method", - "parameters": [ { - "name": "method_id", - "in": "path", - "description": "The unique identifier for the Payment Method", + "name": "maximum_amount", + "in": "query", + "description": "The maximum amount accepted for processing by the particular payment method.", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } - } - ], - "responses": { - "200": { - "description": "Payment Method deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentMethodDeleteResponse" - } - } + }, + { + "name": "recurring_payment_enabled", + "in": "query", + "description": "Indicates whether the payment method is eligible for recurring payments", + "required": true, + "schema": { + "type": "boolean" } }, - "404": { - "description": "Payment Method does not exist in records" - } - }, - "security": [ { - "api_key": [] + "name": "installment_payment_enabled", + "in": "query", + "description": "Indicates whether the payment method is eligible for installment payments", + "required": true, + "schema": { + "type": "boolean" + } } - ] - } - }, - "/payments": { - "post": { - "tags": [ - "Payments" ], - "summary": "Payments - Create", - "description": "Payments - Create\n\nTo process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture", - "operationId": "Create a Payment", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsCreateRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Payment created", + "description": "Payment Methods retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/CustomerPaymentMethodsListResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid Data" + }, + "404": { + "description": "Payment Methods does not exist in records" } }, "security": [ @@ -1596,234 +1675,209 @@ ] } }, - "/payments/list": { + "/disputes/list": { "get": { "tags": [ - "Payments" + "Disputes" ], - "summary": "Payments - List", - "description": "Payments - List\n\nTo list the payments", - "operationId": "List all Payments", + "summary": "Disputes - List Disputes", + "description": "Disputes - List Disputes\nLists all the Disputes for a merchant", + "operationId": "List Disputes", "parameters": [ { - "name": "customer_id", + "name": "limit", "in": "query", - "description": "The identifier for the customer", - "required": true, + "description": "The maximum number of Dispute Objects to include in the response", + "required": false, "schema": { - "type": "string" + "type": "integer", + "format": "int64", + "nullable": true } }, { - "name": "starting_after", + "name": "dispute_status", "in": "query", - "description": "A cursor for use in pagination, fetch the next list after some object", - "required": true, + "description": "The status of dispute", + "required": false, "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/DisputeStatus" + } + ], + "nullable": true } }, { - "name": "ending_before", + "name": "dispute_stage", "in": "query", - "description": "A cursor for use in pagination, fetch the previous list before some object", - "required": true, + "description": "The stage of dispute", + "required": false, "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/DisputeStage" + } + ], + "nullable": true } }, { - "name": "limit", + "name": "reason", "in": "query", - "description": "Limit on the number of objects to return", - "required": true, + "description": "The reason for dispute", + "required": false, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "nullable": true } }, { - "name": "created", + "name": "connector", "in": "query", - "description": "The time at which payment is created", - "required": true, + "description": "The connector linked to dispute", + "required": false, "schema": { "type": "string", - "format": "date-time" + "nullable": true } }, { - "name": "created_lt", + "name": "received_time", "in": "query", - "description": "Time less than the payment created time", - "required": true, + "description": "The time at which dispute is received", + "required": false, "schema": { "type": "string", - "format": "date-time" + "format": "date-time", + "nullable": true } }, { - "name": "created_gt", + "name": "received_time.lt", "in": "query", - "description": "Time greater than the payment created time", - "required": true, + "description": "Time less than the dispute received time", + "required": false, "schema": { "type": "string", - "format": "date-time" + "format": "date-time", + "nullable": true } }, { - "name": "created_lte", + "name": "received_time.gt", "in": "query", - "description": "Time less than or equals to the payment created time", - "required": true, + "description": "Time greater than the dispute received time", + "required": false, "schema": { "type": "string", - "format": "date-time" + "format": "date-time", + "nullable": true } }, { - "name": "created_gte", + "name": "received_time.lte", "in": "query", - "description": "Time greater than or equals to the payment created time", - "required": true, + "description": "Time less than or equals to the dispute received time", + "required": false, "schema": { "type": "string", - "format": "date-time" + "format": "date-time", + "nullable": true } - } - ], - "responses": { - "200": { - "description": "Received payment list" }, - "404": { - "description": "No payments found" - } - }, - "security": [ { - "api_key": [] + "name": "received_time.gte", + "in": "query", + "description": "Time greater than or equals to the dispute received time", + "required": false, + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + } } - ] - } - }, - "/payments/session_tokens": { - "post": { - "tags": [ - "Payments" ], - "summary": "Payments - Session token", - "description": "Payments - Session token\n\nTo create the session object or to get session token for wallets", - "operationId": "Create Session tokens for a Payment", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentsSessionRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Payment session object created or session token was retrieved from wallets", + "description": "The dispute list was retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsSessionResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/DisputeResponse" + } } } } }, - "400": { - "description": "Missing mandatory fields" + "401": { + "description": "Unauthorized request" } }, "security": [ { - "publishable_key": [] + "api_key": [] } ] } }, - "/payments/{payment_id}": { + "/disputes/{dispute_id}": { "get": { "tags": [ - "Payments" + "Disputes" ], - "summary": "Payments - Retrieve", - "description": "Payments - Retrieve\n\nTo retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", - "operationId": "Retrieve a Payment", + "summary": "Disputes - Retrieve Dispute", + "description": "Disputes - Retrieve Dispute\nRetrieves a dispute", + "operationId": "Retrieve a Dispute", "parameters": [ { - "name": "payment_id", + "name": "dispute_id", "in": "path", - "description": "The identifier for payment", + "description": "The identifier for dispute", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentRetrieveBody" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Gets the payment with final status", + "description": "The dispute was retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/DisputeResponse" } } } }, "404": { - "description": "No payment found" + "description": "Dispute does not exist in our records" } }, "security": [ { "api_key": [] - }, - { - "publishable_key": [] } ] - }, + } + }, + "/gsm": { "post": { "tags": [ - "Payments" - ], - "summary": "Payments - Update", - "description": "Payments - Update\n\nTo update the properties of a PaymentIntent object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created", - "operationId": "Update a Payment", - "parameters": [ - { - "name": "payment_id", - "in": "path", - "description": "The identifier for payment", - "required": true, - "schema": { - "type": "string" - } - } + "Gsm" ], + "summary": "Gsm - Create", + "description": "Gsm - Create\n\nCreates a GSM (Global Status Mapping) Rule. A GSM rule is used to map a connector's error message/error code combination during a particular payments flow/sub-flow to Hyperswitch's unified status/error code/error message combination. It is also used to decide the next action in the flow - retry/requeue/do_default", + "operationId": "Create Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsRequest" + "$ref": "#/components/schemas/GsmCreateRequest" } } }, @@ -1831,53 +1885,39 @@ }, "responses": { "200": { - "description": "Payment updated", + "description": "Gsm created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/GsmResponse" } } } }, "400": { - "description": "Missing mandatory fields" + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] - }, - { - "publishable_key": [] + "admin_api_key": [] } ] } }, - "/payments/{payment_id}/cancel": { + "/gsm/delete": { "post": { "tags": [ - "Payments" - ], - "summary": "Payments - Cancel", - "description": "Payments - Cancel\n\nA Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action", - "operationId": "Cancel a Payment", - "parameters": [ - { - "name": "payment_id", - "in": "path", - "description": "The identifier for payment", - "required": true, - "schema": { - "type": "string" - } - } + "Gsm" ], + "summary": "Gsm - Delete", + "description": "Gsm - Delete\n\nDeletes a Gsm Rule", + "operationId": "Delete Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsCancelRequest" + "$ref": "#/components/schemas/GsmDeleteRequest" } } }, @@ -1885,43 +1925,39 @@ }, "responses": { "200": { - "description": "Payment canceled" + "description": "Gsm deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GsmDeleteResponse" + } + } + } }, "400": { - "description": "Missing mandatory fields" + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/payments/{payment_id}/capture": { + "/gsm/get": { "post": { "tags": [ - "Payments" - ], - "summary": "Payments - Capture", - "description": "Payments - Capture\n\nTo capture the funds for an uncaptured payment", - "operationId": "Capture a Payment", - "parameters": [ - { - "name": "payment_id", - "in": "path", - "description": "The identifier for payment", - "required": true, - "schema": { - "type": "string" - } - } + "Gsm" ], + "summary": "Gsm - Get", + "description": "Gsm - Get\n\nRetrieves a Gsm Rule", + "operationId": "Retrieve Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsCaptureRequest" + "$ref": "#/components/schemas/GsmRetrieveRequest" } } }, @@ -1929,50 +1965,39 @@ }, "responses": { "200": { - "description": "Payment captured", + "description": "Gsm retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/GsmResponse" } } } }, "400": { - "description": "Missing mandatory fields" + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] + "admin_api_key": [] } ] } }, - "/payments/{payment_id}/confirm": { + "/gsm/update": { "post": { "tags": [ - "Payments" - ], - "summary": "Payments - Confirm", - "description": "Payments - Confirm\n\nThis API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments Create API", - "operationId": "Confirm a Payment", - "parameters": [ - { - "name": "payment_id", - "in": "path", - "description": "The identifier for payment", - "required": true, - "schema": { - "type": "string" - } - } + "Gsm" ], + "summary": "Gsm - Update", + "description": "Gsm - Update\n\nUpdates a Gsm Rule", + "operationId": "Update Gsm Rule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsRequest" + "$ref": "#/components/schemas/GsmUpdateRequest" } } }, @@ -1980,60 +2005,58 @@ }, "responses": { "200": { - "description": "Payment confirmed", + "description": "Gsm updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/GsmResponse" } } } }, "400": { - "description": "Missing mandatory fields" + "description": "Missing Mandatory fields" } }, "security": [ { - "api_key": [] - }, - { - "publishable_key": [] + "admin_api_key": [] } ] } }, - "/payouts/create": { + "/mandates/revoke/{mandate_id}": { "post": { "tags": [ - "Payouts" + "Mandates" ], - "summary": "Payouts - Create", - "description": "Payouts - Create", - "operationId": "Create a Payout", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayoutCreateRequest" - } + "summary": "Mandates - Revoke Mandate", + "description": "Mandates - Revoke Mandate\n\nRevokes a mandate created using the Payments/Create API", + "operationId": "Revoke a Mandate", + "parameters": [ + { + "name": "mandate_id", + "in": "path", + "description": "The identifier for a mandate", + "required": true, + "schema": { + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { - "description": "Payout created", + "description": "The mandate was revoked successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/MandateRevokedResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Mandate does not exist in our records" } }, "security": [ @@ -2043,19 +2066,19 @@ ] } }, - "/payouts/{payout_id}": { + "/mandates/{mandate_id}": { "get": { "tags": [ - "Payouts" + "Mandates" ], - "summary": "Payouts - Retrieve", - "description": "Payouts - Retrieve", - "operationId": "Retrieve a Payout", - "parameters": [ + "summary": "Mandates - Retrieve Mandate", + "description": "Mandates - Retrieve Mandate\n\nRetrieves a mandate created using the Payments/Create API", + "operationId": "Retrieve a Mandate", + "parameters": [ { - "name": "payout_id", + "name": "mandate_id", "in": "path", - "description": "The identifier for payout]", + "description": "The identifier for mandate", "required": true, "schema": { "type": "string" @@ -2064,17 +2087,17 @@ ], "responses": { "200": { - "description": "Payout retrieved", + "description": "The mandate was retrieved successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/MandateResponse" } } } }, "404": { - "description": "Payout does not exist in our records" + "description": "Mandate does not exist in our records" } }, "security": [ @@ -2082,19 +2105,21 @@ "api_key": [] } ] - }, - "post": { + } + }, + "/payment_link/{payment_link_id}": { + "get": { "tags": [ - "Payouts" + "Payments" ], - "summary": "Payouts - Update", - "description": "Payouts - Update", - "operationId": "Update a Payout", + "summary": "Payments Link - Retrieve", + "description": "Payments Link - Retrieve\n\nTo retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", + "operationId": "Retrieve a Payment Link", "parameters": [ { - "name": "payout_id", + "name": "payment_link_id", "in": "path", - "description": "The identifier for payout]", + "description": "The identifier for payment link", "required": true, "schema": { "type": "string" @@ -2105,7 +2130,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateRequest" + "$ref": "#/components/schemas/RetrievePaymentLinkRequest" } } }, @@ -2113,50 +2138,58 @@ }, "responses": { "200": { - "description": "Payout updated", + "description": "Gets details regarding payment link", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/RetrievePaymentLinkResponse" } } } }, - "400": { - "description": "Missing Mandatory fields" + "404": { + "description": "No payment link found" } }, "security": [ { "api_key": [] + }, + { + "publishable_key": [] } ] } }, - "/payouts/{payout_id}/cancel": { + "/payment_methods": { "post": { "tags": [ - "Payouts" - ], - "summary": "Payouts - Cancel", - "description": "Payouts - Cancel", - "operationId": "Cancel a Payout", - "parameters": [ - { - "name": "payout_id", - "in": "path", - "description": "The identifier for payout", - "required": true, - "schema": { - "type": "string" - } - } + "Payment Methods" ], + "summary": "PaymentMethods - Create", + "description": "PaymentMethods - Create\n\nCreates and stores a payment method against a customer.\nIn case of cards, this API should be used only by PCI compliant merchants.", + "operationId": "Create a Payment Method", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutActionRequest" + "$ref": "#/components/schemas/PaymentMethodCreate" + }, + "examples": { + "Save a card": { + "value": { + "card": { + "card_exp_month": "11", + "card_exp_year": "25", + "card_holder_name": "John Doe", + "card_number": "4242424242424242" + }, + "customer_id": "{{customer_id}}", + "payment_method": "card", + "payment_method_issuer": "Visa", + "payment_method_type": "credit" + } + } } } }, @@ -2164,17 +2197,17 @@ }, "responses": { "200": { - "description": "Payout cancelled", + "description": "Payment Method Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/PaymentMethodResponse" } } } }, "400": { - "description": "Missing Mandatory fields" + "description": "Invalid Data" } }, "security": [ @@ -2184,48 +2217,38 @@ ] } }, - "/payouts/{payout_id}/fulfill": { - "post": { + "/payment_methods/{method_id}": { + "get": { "tags": [ - "Payouts" + "Payment Methods" ], - "summary": "Payouts - Fulfill", - "description": "Payouts - Fulfill", - "operationId": "Fulfill a Payout", + "summary": "Payment Method - Retrieve", + "description": "Payment Method - Retrieve\n\nRetrieves a payment method of a customer.", + "operationId": "Retrieve a Payment method", "parameters": [ { - "name": "payout_id", + "name": "method_id", "in": "path", - "description": "The identifier for payout", + "description": "The unique identifier for the Payment Method", "required": true, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayoutActionRequest" - } - } - }, - "required": true - }, "responses": { "200": { - "description": "Payout fulfilled", + "description": "Payment Method retrieved", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayoutCreateResponse" + "$ref": "#/components/schemas/PaymentMethodResponse" } } } }, - "400": { - "description": "Missing Mandatory fields" + "404": { + "description": "Payment Method does not exist in records" } }, "security": [ @@ -2233,61 +2256,30 @@ "api_key": [] } ] - } - }, - "/refunds": { + }, "post": { "tags": [ - "Refunds" + "Payment Methods" ], - "summary": "Refunds - Create", - "description": "Refunds - Create\n\nTo create a refund against an already processed payment", - "operationId": "Create a Refund", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefundRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Refund created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefundResponse" - } - } - } - }, - "400": { - "description": "Missing Mandatory fields" - } - }, - "security": [ + "summary": "Payment Method - Update", + "description": "Payment Method - Update\n\nUpdate an existing payment method of a customer.\nThis API is useful for use cases such as updating the card number for expired cards to prevent discontinuity in recurring payments.", + "operationId": "Update a Payment method", + "parameters": [ { - "api_key": [] + "name": "method_id", + "in": "path", + "description": "The unique identifier for the Payment Method", + "required": true, + "schema": { + "type": "string" + } } - ] - } - }, - "/refunds/list": { - "post": { - "tags": [ - "Refunds" ], - "summary": "Refunds - List", - "description": "Refunds - List\n\nTo list the refunds associated with a payment_id or with the merchant, if payment_id is not provided", - "operationId": "List all Refunds", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundListRequest" + "$ref": "#/components/schemas/PaymentMethodUpdate" } } }, @@ -2295,14 +2287,17 @@ }, "responses": { "200": { - "description": "List of refunds", + "description": "Payment Method updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundListResponse" + "$ref": "#/components/schemas/PaymentMethodResponse" } } } + }, + "404": { + "description": "Payment Method does not exist in records" } }, "security": [ @@ -2310,21 +2305,19 @@ "api_key": [] } ] - } - }, - "/refunds/{refund_id}": { - "get": { + }, + "delete": { "tags": [ - "Refunds" + "Payment Methods" ], - "summary": "Refunds - Retrieve (GET)", - "description": "Refunds - Retrieve (GET)\n\nTo retrieve the properties of a Refund. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", - "operationId": "Retrieve a Refund", + "summary": "Payment Method - Delete", + "description": "Payment Method - Delete\n\nDeletes a payment method of a customer.", + "operationId": "Delete a Payment method", "parameters": [ { - "name": "refund_id", + "name": "method_id", "in": "path", - "description": "The identifier for refund", + "description": "The unique identifier for the Payment Method", "required": true, "schema": { "type": "string" @@ -2333,17 +2326,17 @@ ], "responses": { "200": { - "description": "Refund retrieved", + "description": "Payment Method deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundResponse" + "$ref": "#/components/schemas/PaymentMethodDeleteResponse" } } } }, "404": { - "description": "Refund does not exist in our records" + "description": "Payment Method does not exist in records" } }, "security": [ @@ -2351,30 +2344,160 @@ "api_key": [] } ] - }, + } + }, + "/payments": { "post": { "tags": [ - "Refunds" - ], - "summary": "Refunds - Update", - "description": "Refunds - Update\n\nTo update the properties of a Refund object. This may include attaching a reason for the refund or metadata fields", - "operationId": "Update a Refund", - "parameters": [ - { - "name": "refund_id", - "in": "path", - "description": "The identifier for refund", - "required": true, - "schema": { - "type": "string" - } - } + "Payments" ], + "summary": "Payments - Create", + "description": "Payments - Create\n\n**Creates a payment object when amount and currency are passed.** This API is also used to create a mandate by passing the `mandate_object`.\n\nTo completely process a payment you will have to create a payment, attach a payment method, confirm and capture funds.\n\nDepending on the user journey you wish to achieve, you may opt to complete all the steps in a single request by attaching a payment method, setting `confirm=true` and `capture_method = automatic` in the *Payments/Create API* request or you could use the following sequence of API requests to achieve the same:\n\n1. Payments - Create\n\n2. Payments - Update\n\n3. Payments - Confirm\n\n4. Payments - Capture.\n\nUse the client secret returned in this API along with your publishable key to make subsequent API calls from your client", + "operationId": "Create a Payment", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundUpdateRequest" + "$ref": "#/components/schemas/PaymentsCreateRequest" + }, + "examples": { + "Create a 3DS payment": { + "value": { + "amount": 6540, + "authentication_type": "three_ds", + "currency": "USD" + } + }, + "Create a manual capture payment": { + "value": { + "amount": 6540, + "billing": { + "address": { + "city": "San Fransico", + "country": "US", + "first_name": "joseph", + "last_name": "Doe", + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "state": "California", + "zip": "94122" + }, + "phone": { + "country_code": "+91", + "number": "8056594427" + } + }, + "currency": "USD", + "customer": { + "id": "cus_abcdefgh" + } + } + }, + "Create a payment and save the card": { + "value": { + "amount": 6540, + "authentication_type": "no_three_ds", + "confirm": true, + "currency": "USD", + "customer_id": "StripeCustomer123", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "setup_future_usage": "off_session" + } + }, + "Create a payment using an already saved card's token": { + "value": { + "amount": 6540, + "card_cvc": "123", + "client_secret": "{{client_secret}}", + "confirm": true, + "currency": "USD", + "payment_method": "card", + "payment_token": "{{payment_token}}" + } + }, + "Create a payment with customer details and metadata": { + "value": { + "amount": 6540, + "currency": "USD", + "customer": { + "email": "john@example.com", + "id": "cus_abcdefgh", + "name": "John Dough", + "phone": "9999999999" + }, + "description": "Its my first payment request", + "metadata": { + "udf1": "some-value", + "udf2": "some-value" + }, + "payment_id": "abcdefghijklmnopqrstuvwxyz", + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS" + } + }, + "Create a payment with minimal fields": { + "value": { + "amount": 6540, + "currency": "USD" + } + }, + "Create a recurring payment with mandate_id": { + "value": { + "amount": 6540, + "authentication_type": "no_three_ds", + "confirm": true, + "currency": "USD", + "customer_id": "StripeCustomer", + "mandate_id": "{{mandate_id}}", + "off_session": true + } + }, + "Create a setup mandate payment": { + "value": { + "amount": 6540, + "authentication_type": "no_three_ds", + "confirm": true, + "currency": "USD", + "customer_id": "StripeCustomer123", + "mandate_data": { + "customer_acceptance": { + "acceptance_type": "offline", + "accepted_at": "1963-05-03T04:07:52.723Z", + "online": { + "ip_address": "127.0.0.1", + "user_agent": "amet irure esse" + } + }, + "mandate_type": { + "single_use": { + "amount": 6540, + "currency": "USD" + } + } + }, + "payment_method": "card", + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "setup_future_usage": "off_session" + } + } } } }, @@ -2382,11 +2505,11 @@ }, "responses": { "200": { - "description": "Refund updated", + "description": "Payment created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RefundResponse" + "$ref": "#/components/schemas/PaymentsResponse" } } } @@ -2401,794 +2524,2281 @@ } ] } - } - }, - "components": { - "schemas": { - "AcceptanceType": { - "type": "string", - "enum": [ - "online", - "offline" - ] - }, - "AcceptedCountries": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "enable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } + }, + "/payments/list": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments - List", + "description": "Payments - List\n\nTo list the *payments*", + "operationId": "List all Payments", + "parameters": [ + { + "name": "customer_id", + "in": "query", + "description": "The identifier for the customer", + "required": true, + "schema": { + "type": "string" } }, { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "disable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } + "name": "starting_after", + "in": "query", + "description": "A cursor for use in pagination, fetch the next list after some object", + "required": true, + "schema": { + "type": "string" } }, { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "all_accepted" - ] - } + "name": "ending_before", + "in": "query", + "description": "A cursor for use in pagination, fetch the previous list before some object", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Limit on the number of objects to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "created", + "in": "query", + "description": "The time at which payment is created", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_lt", + "in": "query", + "description": "Time less than the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_gt", + "in": "query", + "description": "Time greater than the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_lte", + "in": "query", + "description": "Time less than or equals to the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "created_gte", + "in": "query", + "description": "Time greater than or equals to the payment created time", + "required": true, + "schema": { + "type": "string", + "format": "date-time" } } ], - "discriminator": { - "propertyName": "type" - } - }, - "AcceptedCurrencies": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "enable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + "responses": { + "200": { + "description": "Successfully retrieved a payment list", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentListResponse" + } } } } }, + "404": { + "description": "No payments found" + } + }, + "security": [ { - "type": "object", - "required": [ - "type", - "list" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "disable_only" - ] - }, - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" + "api_key": [] + } + ] + } + }, + "/payments/session_tokens": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Session token", + "description": "Payments - Session token\n\nCreates a session object or a session token for wallets like Apple Pay, Google Pay, etc. These tokens are used by Hyperswitch's SDK to initiate these wallets' SDK.", + "operationId": "Create Session tokens for a Payment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsSessionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment session object created or session token was retrieved from wallets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsSessionResponse" } } } }, + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "all_accepted" - ] - } + "publishable_key": [] + } + ] + } + }, + "/payments/{payment_id}": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments - Retrieve", + "description": "Payments - Retrieve\n\nRetrieves a Payment. This API can also be used to get the status of a previously initiated payment or next action for an ongoing payment", + "operationId": "Retrieve a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" } } ], - "discriminator": { - "propertyName": "type" - } - }, - "AchBankTransfer": { - "type": "object", - "required": [ - "bank_account_number", - "bank_routing_number" - ], - "properties": { - "bank_name": { - "type": "string", - "description": "Bank name", - "example": "Deutsche Bank", - "nullable": true - }, - "bank_country_code": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentRetrieveBody" } - ], - "nullable": true + } }, - "bank_city": { - "type": "string", - "description": "Bank city", - "example": "California", - "nullable": true + "required": true + }, + "responses": { + "200": { + "description": "Gets the payment with final status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - "bank_account_number": { - "type": "string", - "description": "Bank account number is an unique identifier assigned by a bank to a customer.", - "example": "000123456" + "404": { + "description": "No payment found" + } + }, + "security": [ + { + "api_key": [] }, - "bank_routing_number": { - "type": "string", - "description": "[9 digits] Routing number - used in USA for identifying a specific bank.", - "example": "110000000" + { + "publishable_key": [] } - } + ] }, - "AchBillingDetails": { - "type": "object", - "required": [ - "email" + "post": { + "tags": [ + "Payments" ], - "properties": { - "email": { - "type": "string", - "description": "The Email ID for ACH billing", - "example": "example@me.com" + "summary": "Payments - Update", + "description": "Payments - Update\n\nTo update the properties of a *PaymentIntent* object. This may include attaching a payment method, or attaching customer object or metadata fields after the Payment is created", + "operationId": "Update a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "AchTransfer": { - "type": "object", - "required": [ - "account_number", - "bank_name", - "routing_number", - "swift_code" ], - "properties": { - "account_number": { - "type": "string", - "example": "122385736258" + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsUpdateRequest" + }, + "examples": { + "Update the payment amount": { + "value": { + "amount": 7654 + } + }, + "Update the shipping address": { + "value": { + "shipping": { + "address": { + "city": "San Fransico", + "country": "US", + "first_name": "joseph", + "last_name": "Doe", + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "state": "California", + "zip": "94122" + }, + "phone": { + "country_code": "+91", + "number": "8056594427" + } + } + } + } + } + } }, - "bank_name": { - "type": "string" + "required": true + }, + "responses": { + "200": { + "description": "Payment updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - "routing_number": { - "type": "string", - "example": "012" + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ + { + "api_key": [] }, - "swift_code": { - "type": "string", - "example": "234" + { + "publishable_key": [] } - } - }, - "Address": { - "type": "object", - "properties": { - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" + ] + } + }, + "/payments/{payment_id}/cancel": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Cancel", + "description": "Payments - Cancel\n\nA Payment could can be cancelled when it is in one of these statuses: `requires_payment_method`, `requires_capture`, `requires_confirmation`, `requires_customer_action`.", + "operationId": "Cancel a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsCancelRequest" + }, + "examples": { + "Cancel the payment with cancellation reason": { + "value": { + "cancellation_reason": "requested_by_customer" + } + }, + "Cancel the payment with minimal fields": { + "value": {} + } } - ], - "nullable": true + } }, - "phone": { - "allOf": [ - { - "$ref": "#/components/schemas/PhoneDetails" - } - ], - "nullable": true - } - } - }, - "AddressDetails": { - "type": "object", - "properties": { - "city": { - "type": "string", - "description": "The address city", - "example": "New York", - "nullable": true, - "maxLength": 50 + "required": true + }, + "responses": { + "200": { + "description": "Payment canceled" }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/payments/{payment_id}/capture": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Capture", + "description": "Payments - Capture\n\nTo capture the funds for an uncaptured payment", + "operationId": "Capture a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsCaptureRequest" + }, + "examples": { + "Capture partial amount": { + "value": { + "amount_to_capture": 654 + } + }, + "Capture the full amount": { + "value": {} + } } - ], - "nullable": true + } }, - "line1": { - "type": "string", - "description": "The first line of the address", - "example": "123, King Street", - "nullable": true, - "maxLength": 200 - }, - "line2": { - "type": "string", - "description": "The second line of the address", - "example": "Powelson Avenue", - "nullable": true, - "maxLength": 50 - }, - "line3": { - "type": "string", - "description": "The third line of the address", - "example": "Bridgewater", - "nullable": true, - "maxLength": 50 - }, - "zip": { - "type": "string", - "description": "The zip/postal code for the address", - "example": "08807", - "nullable": true, - "maxLength": 50 - }, - "state": { - "type": "string", - "description": "The address state", - "example": "New York", - "nullable": true - }, - "first_name": { - "type": "string", - "description": "The first name for the address", - "example": "John", - "nullable": true, - "maxLength": 255 + "required": true + }, + "responses": { + "200": { + "description": "Payment captured", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - "last_name": { - "type": "string", - "description": "The last name for the address", - "example": "Doe", - "nullable": true, - "maxLength": 255 + "400": { + "description": "Missing mandatory fields" } - } - }, - "AirwallexData": { - "type": "object", - "properties": { - "payload": { - "type": "string", - "description": "payload required by airwallex", - "nullable": true + }, + "security": [ + { + "api_key": [] } - } - }, - "AlfamartVoucherData": { - "type": "object", - "required": [ - "first_name", - "last_name", - "email" + ] + } + }, + "/payments/{payment_id}/confirm": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "first_name": { - "type": "string", - "description": "The billing first name for Alfamart", - "example": "Jane" - }, - "last_name": { - "type": "string", - "description": "The billing second name for Alfamart", - "example": "Doe" - }, - "email": { - "type": "string", - "description": "The Email ID for Alfamart", - "example": "example@me.com" + "summary": "Payments - Confirm", + "description": "Payments - Confirm\n\n**Use this API to confirm the payment and forward the payment to the payment processor.**\n\nAlternatively you can confirm the payment within the *Payments/Create* API by setting `confirm=true`. After confirmation, the payment could either:\n\n1. fail with `failed` status or\n\n2. transition to a `requires_customer_action` status with a `next_action` block or\n\n3. succeed with either `succeeded` in case of automatic capture or `requires_capture` in case of manual capture", + "operationId": "Confirm a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "AliPayHkRedirection": { - "type": "object" - }, - "AliPayQr": { - "type": "object" - }, - "AliPayRedirection": { - "type": "object" - }, - "AmountInfo": { - "type": "object", - "required": [ - "label", - "amount" ], - "properties": { - "label": { - "type": "string", - "description": "The label must be the name of the merchant." + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsConfirmRequest" + }, + "examples": { + "Confirm a payment with payment method data": { + "value": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "payment_method_type": "credit" + } + } + } + } }, - "type": { - "type": "string", - "description": "A value that indicates whether the line item(Ex: total, tax, discount, or grand total) is final or pending.", - "nullable": true + "required": true + }, + "responses": { + "200": { + "description": "Payment confirmed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - "amount": { - "type": "string", - "description": "The total amount for the payment" + "400": { + "description": "Missing mandatory fields" } - } - }, - "ApiKeyExpiration": { - "oneOf": [ + }, + "security": [ { - "type": "string", - "enum": [ - "never" - ] + "api_key": [] }, { - "type": "string", - "format": "date-time" + "publishable_key": [] } ] - }, - "ApplePayPaymentRequest": { - "type": "object", - "required": [ - "country_code", - "currency_code", - "total" + } + }, + "/payments/{payment_id}/incremental_authorization": { + "post": { + "tags": [ + "Payments" ], - "properties": { - "country_code": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "currency_code": { - "$ref": "#/components/schemas/Currency" - }, - "total": { - "$ref": "#/components/schemas/AmountInfo" - }, - "merchant_capabilities": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of merchant capabilities(ex: whether capable of 3ds or no-3ds)", - "nullable": true - }, - "supported_networks": { - "type": "array", - "items": { + "summary": "Payments - Incremental Authorization", + "description": "Payments - Incremental Authorization\n\nAuthorized amount for a payment can be incremented if it is in status: requires_capture", + "operationId": "Increment authorized amount for a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { "type": "string" - }, - "description": "The list of supported networks", - "nullable": true - }, - "merchant_identifier": { - "type": "string", - "nullable": true + } } - } - }, - "ApplePayRedirectData": { - "type": "object" - }, - "ApplePaySessionResponse": { - "oneOf": [ - { - "$ref": "#/components/schemas/ThirdPartySdkSessionResponse" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsIncrementalAuthorizationRequest" + } + } }, - { - "$ref": "#/components/schemas/NoThirdPartySdkSessionResponse" + "required": true + }, + "responses": { + "200": { + "description": "Payment authorized amount incremented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } }, - { - "type": "object", - "default": null, - "nullable": true + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ + { + "api_key": [] } ] - }, - "ApplePayThirdPartySdkData": { - "type": "object" - }, - "ApplePayWalletData": { - "type": "object", - "required": [ - "payment_data", - "payment_method", - "transaction_identifier" + } + }, + "/payouts/create": { + "post": { + "tags": [ + "Payouts" ], - "properties": { - "payment_data": { - "type": "string", - "description": "The payment data of Apple pay" + "summary": "Payouts - Create", + "description": "Payouts - Create", + "operationId": "Create a Payout", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateRequest" + } + } }, - "payment_method": { - "$ref": "#/components/schemas/ApplepayPaymentMethod" + "required": true + }, + "responses": { + "200": { + "description": "Payout created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" + } + } + } }, - "transaction_identifier": { - "type": "string", - "description": "The unique identifier for the transaction" + "400": { + "description": "Missing Mandatory fields" } - } - }, - "ApplepayConnectorMetadataRequest": { - "type": "object", - "properties": { - "session_token_data": { - "allOf": [ - { - "$ref": "#/components/schemas/SessionTokenInfo" - } - ], - "nullable": true + }, + "security": [ + { + "api_key": [] } - } - }, - "ApplepayPaymentMethod": { - "type": "object", - "required": [ - "display_name", - "network", - "type" + ] + } + }, + "/payouts/{payout_id}": { + "get": { + "tags": [ + "Payouts" ], - "properties": { - "display_name": { - "type": "string", - "description": "The name to be displayed on Apple Pay button" - }, - "network": { - "type": "string", - "description": "The network of the Apple pay payment method" - }, - "type": { - "type": "string", - "description": "The type of the payment method" + "summary": "Payouts - Retrieve", + "description": "Payouts - Retrieve", + "operationId": "Retrieve a Payout", + "parameters": [ + { + "name": "payout_id", + "in": "path", + "description": "The identifier for payout]", + "required": true, + "schema": { + "type": "string" + } } - } - }, - "ApplepaySessionTokenResponse": { - "type": "object", - "required": [ - "session_token_data", - "connector", - "delayed_session_token", - "sdk_next_action" ], - "properties": { - "session_token_data": { - "$ref": "#/components/schemas/ApplePaySessionResponse" - }, - "payment_request_data": { - "allOf": [ - { - "$ref": "#/components/schemas/ApplePayPaymentRequest" + "responses": { + "200": { + "description": "Payout retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" + } } - ], - "nullable": true - }, - "connector": { - "type": "string", - "description": "The session token is w.r.t this connector" - }, - "delayed_session_token": { - "type": "boolean", - "description": "Identifier for the delayed session response" - }, - "sdk_next_action": { - "$ref": "#/components/schemas/SdkNextAction" - }, - "connector_reference_id": { - "type": "string", - "description": "The connector transaction id", - "nullable": true - }, - "connector_sdk_public_key": { - "type": "string", - "description": "The public key id is to invoke third party sdk", - "nullable": true + } }, - "connector_merchant_id": { - "type": "string", - "description": "The connector merchant id", - "nullable": true + "404": { + "description": "Payout does not exist in our records" + } + }, + "security": [ + { + "api_key": [] } - } - }, - "AttemptStatus": { - "type": "string", - "enum": [ - "started", - "authentication_failed", - "router_declined", - "authentication_pending", - "authentication_successful", - "authorized", - "authorization_failed", - "charged", - "authorizing", - "cod_initiated", - "voided", - "void_initiated", - "capture_initiated", - "capture_failed", - "void_failed", - "auto_refunded", - "partial_charged", - "partial_charged_and_chargeable", - "unresolved", - "pending", - "failure", - "payment_method_awaited", - "confirmation_awaited", - "device_data_collection_pending" - ] - }, - "AuthenticationType": { - "type": "string", - "enum": [ - "three_ds", - "no_three_ds" - ] - }, - "AuthorizationStatus": { - "type": "string", - "enum": [ - "success", - "failure", - "processing", - "unresolved" ] }, - "BacsBankTransfer": { - "type": "object", - "required": [ - "bank_account_number", - "bank_sort_code" + "post": { + "tags": [ + "Payouts" ], - "properties": { - "bank_name": { - "type": "string", - "description": "Bank name", - "example": "Deutsche Bank", - "nullable": true - }, - "bank_country_code": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" + "summary": "Payouts - Update", + "description": "Payouts - Update", + "operationId": "Update a Payout", + "parameters": [ + { + "name": "payout_id", + "in": "path", + "description": "The identifier for payout]", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateRequest" } - ], - "nullable": true + } }, - "bank_city": { - "type": "string", - "description": "Bank city", - "example": "California", - "nullable": true - }, - "bank_account_number": { - "type": "string", - "description": "Bank account number is an unique identifier assigned by a bank to a customer.", - "example": "000123456" + "required": true + }, + "responses": { + "200": { + "description": "Payout updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" + } + } + } }, - "bank_sort_code": { - "type": "string", - "description": "[6 digits] Sort Code - used in UK and Ireland for identifying a bank and it's branches.", - "example": "98-76-54" + "400": { + "description": "Missing Mandatory fields" } - } - }, - "BacsBankTransferInstructions": { - "type": "object", - "required": [ - "account_holder_name", - "account_number", - "sort_code" - ], - "properties": { - "account_holder_name": { - "type": "string", - "example": "Jane Doe" - }, - "account_number": { - "type": "string", - "example": "10244123908" - }, - "sort_code": { - "type": "string", - "example": "012" + }, + "security": [ + { + "api_key": [] } - } - }, - "Bank": { - "oneOf": [ + ] + } + }, + "/payouts/{payout_id}/cancel": { + "post": { + "tags": [ + "Payouts" + ], + "summary": "Payouts - Cancel", + "description": "Payouts - Cancel", + "operationId": "Cancel a Payout", + "parameters": [ { - "$ref": "#/components/schemas/AchBankTransfer" + "name": "payout_id", + "in": "path", + "description": "The identifier for payout", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutActionRequest" + } + } }, - { - "$ref": "#/components/schemas/BacsBankTransfer" + "required": true + }, + "responses": { + "200": { + "description": "Payout cancelled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" + } + } + } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "$ref": "#/components/schemas/SepaBankTransfer" + "api_key": [] } ] - }, - "BankDebitBilling": { - "type": "object", - "required": [ - "name", - "email" + } + }, + "/payouts/{payout_id}/fulfill": { + "post": { + "tags": [ + "Payouts" ], - "properties": { - "name": { - "type": "string", - "description": "The billing name for bank debits", - "example": "John Doe" - }, - "email": { - "type": "string", - "description": "The billing email for bank debits", - "example": "example@example.com" - }, - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" - } - ], - "nullable": true - } - } - }, - "BankDebitData": { - "oneOf": [ + "summary": "Payouts - Fulfill", + "description": "Payouts - Fulfill", + "operationId": "Fulfill a Payout", + "parameters": [ { - "type": "object", - "required": [ - "ach_bank_debit" - ], - "properties": { - "ach_bank_debit": { - "type": "object", - "description": "Payment Method data for Ach bank debit", - "required": [ - "billing_details", - "account_number", - "routing_number", - "card_holder_name", - "bank_account_holder_name", - "bank_name", - "bank_type", - "bank_holder_type" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankDebitBilling" - }, - "account_number": { - "type": "string", - "description": "Account number for ach bank debit payment", - "example": "000123456789" - }, - "routing_number": { - "type": "string", - "description": "Routing number for ach bank debit payment", - "example": "110000000" - }, - "card_holder_name": { - "type": "string", - "example": "John Test" - }, - "bank_account_holder_name": { - "type": "string", - "example": "John Doe" - }, - "bank_name": { - "type": "string", - "example": "ACH" - }, - "bank_type": { - "type": "string", - "example": "Checking" - }, - "bank_holder_type": { - "type": "string", - "example": "Personal" - } - } + "name": "payout_id", + "in": "path", + "description": "The identifier for payout", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutActionRequest" } } }, - { - "type": "object", - "required": [ - "sepa_bank_debit" - ], - "properties": { - "sepa_bank_debit": { - "type": "object", - "required": [ - "billing_details", - "iban", - "bank_account_holder_name" - ], - "properties": { - "billing_details": { - "$ref": "#/components/schemas/BankDebitBilling" - }, - "iban": { - "type": "string", - "description": "International bank account number (iban) for SEPA", - "example": "DE89370400440532013000" - }, - "bank_account_holder_name": { - "type": "string", - "description": "Owner name for bank debit", - "example": "A. Schneider" - } + "required": true + }, + "responses": { + "200": { + "description": "Payout fulfilled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayoutCreateResponse" } } } }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ { - "type": "object", - "required": [ - "becs_bank_debit" + "api_key": [] + } + ] + } + }, + "/refunds": { + "post": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Create", + "description": "Refunds - Create\n\nCreates a refund against an already processed payment. In case of some processors, you can even opt to refund only a partial amount multiple times until the original charge amount has been refunded", + "operationId": "Create a Refund", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundRequest" + }, + "examples": { + "Create an instant refund to refund partial amount": { + "value": { + "amount": 654, + "payment_id": "{{payment_id}}", + "refund_type": "instant" + } + }, + "Create an instant refund to refund the whole amount": { + "value": { + "payment_id": "{{payment_id}}", + "refund_type": "instant" + } + }, + "Create an instant refund with reason": { + "value": { + "amount": 6540, + "payment_id": "{{payment_id}}", + "reason": "Customer returned product", + "refund_type": "instant" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Refund created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/refunds/list": { + "post": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - List", + "description": "Refunds - List\n\nLists all the refunds associated with the merchant or a payment_id if payment_id is not provided", + "operationId": "List all Refunds", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundListRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "List of refunds", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundListResponse" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/refunds/{refund_id}": { + "get": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Retrieve", + "description": "Refunds - Retrieve\n\nRetrieves a Refund. This may be used to get the status of a previously initiated refund", + "operationId": "Retrieve a Refund", + "parameters": [ + { + "name": "refund_id", + "in": "path", + "description": "The identifier for refund", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Refund retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" + } + } + } + }, + "404": { + "description": "Refund does not exist in our records" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "Refunds" + ], + "summary": "Refunds - Update", + "description": "Refunds - Update\n\nUpdates the properties of a Refund object. This API can be used to attach a reason for the refund or metadata fields", + "operationId": "Update a Refund", + "parameters": [ + { + "name": "refund_id", + "in": "path", + "description": "The identifier for refund", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundUpdateRequest" + }, + "examples": { + "Update refund reason": { + "value": { + "reason": "Paid by mistake" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Refund updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefundResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/routing": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - List", + "description": "Routing - List\n\nList all routing configs", + "operationId": "List routing configs", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "The number of records to be returned", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + }, + { + "name": "offset", + "in": "query", + "description": "The record offset from which to start gathering of results", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "nullable": true, + "minimum": 0 + } + }, + { + "name": "profile_id", + "in": "query", + "description": "The unique identifier for a merchant profile", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched routing configs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingKind" + } + } + } + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + }, + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Create", + "description": "Routing - Create\n\nCreate a routing config", + "operationId": "Create a routing config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Routing config created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Request body is malformed" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/active": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve Config", + "description": "Routing - Retrieve Config\n\nRetrieve active config", + "operationId": "Retrieve active config", + "parameters": [ + { + "name": "profile_id", + "in": "query", + "description": "The unique identifier for a merchant profile", + "required": false, + "schema": { + "type": "string", + "nullable": true + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved active config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkedRoutingConfigRetrieveResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/deactivate": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Deactivate", + "description": "Routing - Deactivate\n\nDeactivates a routing config", + "operationId": "Deactivate a routing config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successfully deactivated routing config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Malformed request" + }, + "403": { + "description": "Malformed request" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/default": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve Default Config", + "description": "Routing - Retrieve Default Config\n\nRetrieve default fallback config", + "operationId": "Retrieve default fallback config", + "responses": { + "200": { + "description": "Successfully retrieved default config", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + } + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + }, + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Update Default Config", + "description": "Routing - Update Default Config\n\nUpdate default fallback config", + "operationId": "Update default fallback config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successfully updated default config", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + } + }, + "400": { + "description": "Malformed request" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/default/profile": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve Default For Profile", + "description": "Routing - Retrieve Default For Profile\n\nRetrieve default config for profiles", + "operationId": "Retrieve default configs for all profiles", + "responses": { + "200": { + "description": "Successfully retrieved default config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileDefaultRoutingConfig" + } + } + } + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/default/profile/{profile_id}": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Update Default For Profile", + "description": "Routing - Update Default For Profile\n\nUpdate default config for profiles", + "operationId": "Update default configs for all profiles", + "parameters": [ + { + "name": "profile_id", + "in": "path", + "description": "The unique identifier for a profile", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successfully updated default config for profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileDefaultRoutingConfig" + } + } + } + }, + "400": { + "description": "Malformed request" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/{algorithm_id}": { + "get": { + "tags": [ + "Routing" + ], + "summary": "Routing - Retrieve", + "description": "Routing - Retrieve\n\nRetrieve a routing algorithm", + "operationId": "Retrieve a routing config", + "parameters": [ + { + "name": "algorithm_id", + "in": "path", + "description": "The unique identifier for a config", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched routing config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantRoutingAlgorithm" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/routing/{algorithm_id}/activate": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Activate config", + "description": "Routing - Activate config\n\nActivate a routing config", + "operationId": "Activate a routing config", + "parameters": [ + { + "name": "algorithm_id", + "in": "path", + "description": "The unique identifier for a config", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Routing config activated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Resource missing" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + } + }, + "components": { + "schemas": { + "AcceptanceType": { + "type": "string", + "description": "This is used to indicate if the mandate was accepted online or offline", + "enum": [ + "online", + "offline" + ] + }, + "AcceptedCountries": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "list" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "enable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + }, + { + "type": "object", + "required": [ + "type", + "list" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "disable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "all_accepted" + ] + } + } + } + ], + "description": "Object to filter the customer countries for which the payment method is displayed", + "discriminator": { + "propertyName": "type" + } + }, + "AcceptedCurrencies": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "list" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "enable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } + } + } + }, + { + "type": "object", + "required": [ + "type", + "list" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "disable_only" + ] + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "all_accepted" + ] + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "AchBankTransfer": { + "type": "object", + "required": [ + "bank_account_number", + "bank_routing_number" + ], + "properties": { + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + }, + "bank_account_number": { + "type": "string", + "description": "Bank account number is an unique identifier assigned by a bank to a customer.", + "example": "000123456" + }, + "bank_routing_number": { + "type": "string", + "description": "[9 digits] Routing number - used in USA for identifying a specific bank.", + "example": "110000000" + } + } + }, + "AchBillingDetails": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "description": "The Email ID for ACH billing", + "example": "example@me.com" + } + } + }, + "AchTransfer": { + "type": "object", + "required": [ + "account_number", + "bank_name", + "routing_number", + "swift_code" + ], + "properties": { + "account_number": { + "type": "string", + "example": "122385736258" + }, + "bank_name": { + "type": "string" + }, + "routing_number": { + "type": "string", + "example": "012" + }, + "swift_code": { + "type": "string", + "example": "234" + } + } + }, + "Address": { + "type": "object", + "properties": { + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "phone": { + "allOf": [ + { + "$ref": "#/components/schemas/PhoneDetails" + } + ], + "nullable": true + } + } + }, + "AddressDetails": { + "type": "object", + "description": "Address details", + "properties": { + "city": { + "type": "string", + "description": "The address city", + "example": "New York", + "nullable": true, + "maxLength": 50 + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "line1": { + "type": "string", + "description": "The first line of the address", + "example": "123, King Street", + "nullable": true, + "maxLength": 200 + }, + "line2": { + "type": "string", + "description": "The second line of the address", + "example": "Powelson Avenue", + "nullable": true, + "maxLength": 50 + }, + "line3": { + "type": "string", + "description": "The third line of the address", + "example": "Bridgewater", + "nullable": true, + "maxLength": 50 + }, + "zip": { + "type": "string", + "description": "The zip/postal code for the address", + "example": "08807", + "nullable": true, + "maxLength": 50 + }, + "state": { + "type": "string", + "description": "The address state", + "example": "New York", + "nullable": true + }, + "first_name": { + "type": "string", + "description": "The first name for the address", + "example": "John", + "nullable": true, + "maxLength": 255 + }, + "last_name": { + "type": "string", + "description": "The last name for the address", + "example": "Doe", + "nullable": true, + "maxLength": 255 + } + } + }, + "AirwallexData": { + "type": "object", + "properties": { + "payload": { + "type": "string", + "description": "payload required by airwallex", + "nullable": true + } + } + }, + "AlfamartVoucherData": { + "type": "object", + "required": [ + "first_name", + "last_name", + "email" + ], + "properties": { + "first_name": { + "type": "string", + "description": "The billing first name for Alfamart", + "example": "Jane" + }, + "last_name": { + "type": "string", + "description": "The billing second name for Alfamart", + "example": "Doe" + }, + "email": { + "type": "string", + "description": "The Email ID for Alfamart", + "example": "example@me.com" + } + } + }, + "AliPayHkRedirection": { + "type": "object" + }, + "AliPayQr": { + "type": "object" + }, + "AliPayRedirection": { + "type": "object" + }, + "AmountInfo": { + "type": "object", + "required": [ + "label", + "amount" + ], + "properties": { + "label": { + "type": "string", + "description": "The label must be the name of the merchant." + }, + "type": { + "type": "string", + "description": "A value that indicates whether the line item(Ex: total, tax, discount, or grand total) is final or pending.", + "nullable": true + }, + "amount": { + "type": "string", + "description": "The total amount for the payment" + } + } + }, + "ApiKeyExpiration": { + "oneOf": [ + { + "type": "string", + "enum": [ + "never" + ] + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "ApplePayPaymentRequest": { + "type": "object", + "required": [ + "country_code", + "currency_code", + "total" + ], + "properties": { + "country_code": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "currency_code": { + "$ref": "#/components/schemas/Currency" + }, + "total": { + "$ref": "#/components/schemas/AmountInfo" + }, + "merchant_capabilities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of merchant capabilities(ex: whether capable of 3ds or no-3ds)", + "nullable": true + }, + "supported_networks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of supported networks", + "nullable": true + }, + "merchant_identifier": { + "type": "string", + "nullable": true + } + } + }, + "ApplePayRedirectData": { + "type": "object" + }, + "ApplePaySessionResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/ThirdPartySdkSessionResponse" + }, + { + "$ref": "#/components/schemas/NoThirdPartySdkSessionResponse" + }, + { + "type": "object", + "default": null, + "nullable": true + } + ] + }, + "ApplePayThirdPartySdkData": { + "type": "object" + }, + "ApplePayWalletData": { + "type": "object", + "required": [ + "payment_data", + "payment_method", + "transaction_identifier" + ], + "properties": { + "payment_data": { + "type": "string", + "description": "The payment data of Apple pay" + }, + "payment_method": { + "$ref": "#/components/schemas/ApplepayPaymentMethod" + }, + "transaction_identifier": { + "type": "string", + "description": "The unique identifier for the transaction" + } + } + }, + "ApplepayConnectorMetadataRequest": { + "type": "object", + "properties": { + "session_token_data": { + "allOf": [ + { + "$ref": "#/components/schemas/SessionTokenInfo" + } + ], + "nullable": true + } + } + }, + "ApplepayPaymentMethod": { + "type": "object", + "required": [ + "display_name", + "network", + "type" + ], + "properties": { + "display_name": { + "type": "string", + "description": "The name to be displayed on Apple Pay button" + }, + "network": { + "type": "string", + "description": "The network of the Apple pay payment method" + }, + "type": { + "type": "string", + "description": "The type of the payment method" + } + } + }, + "ApplepaySessionTokenResponse": { + "type": "object", + "required": [ + "session_token_data", + "connector", + "delayed_session_token", + "sdk_next_action" + ], + "properties": { + "session_token_data": { + "$ref": "#/components/schemas/ApplePaySessionResponse" + }, + "payment_request_data": { + "allOf": [ + { + "$ref": "#/components/schemas/ApplePayPaymentRequest" + } + ], + "nullable": true + }, + "connector": { + "type": "string", + "description": "The session token is w.r.t this connector" + }, + "delayed_session_token": { + "type": "boolean", + "description": "Identifier for the delayed session response" + }, + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" + }, + "connector_reference_id": { + "type": "string", + "description": "The connector transaction id", + "nullable": true + }, + "connector_sdk_public_key": { + "type": "string", + "description": "The public key id is to invoke third party sdk", + "nullable": true + }, + "connector_merchant_id": { + "type": "string", + "description": "The connector merchant id", + "nullable": true + } + } + }, + "AttemptStatus": { + "type": "string", + "enum": [ + "started", + "authentication_failed", + "router_declined", + "authentication_pending", + "authentication_successful", + "authorized", + "authorization_failed", + "charged", + "authorizing", + "cod_initiated", + "voided", + "void_initiated", + "capture_initiated", + "capture_failed", + "void_failed", + "auto_refunded", + "partial_charged", + "partial_charged_and_chargeable", + "unresolved", + "pending", + "failure", + "payment_method_awaited", + "confirmation_awaited", + "device_data_collection_pending" + ] + }, + "AuthenticationType": { + "type": "string", + "enum": [ + "three_ds", + "no_three_ds" + ] + }, + "AuthorizationStatus": { + "type": "string", + "enum": [ + "success", + "failure", + "processing", + "unresolved" + ] + }, + "BacsBankTransfer": { + "type": "object", + "required": [ + "bank_account_number", + "bank_sort_code" + ], + "properties": { + "bank_name": { + "type": "string", + "description": "Bank name", + "example": "Deutsche Bank", + "nullable": true + }, + "bank_country_code": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], + "nullable": true + }, + "bank_city": { + "type": "string", + "description": "Bank city", + "example": "California", + "nullable": true + }, + "bank_account_number": { + "type": "string", + "description": "Bank account number is an unique identifier assigned by a bank to a customer.", + "example": "000123456" + }, + "bank_sort_code": { + "type": "string", + "description": "[6 digits] Sort Code - used in UK and Ireland for identifying a bank and it's branches.", + "example": "98-76-54" + } + } + }, + "BacsBankTransferInstructions": { + "type": "object", + "required": [ + "account_holder_name", + "account_number", + "sort_code" + ], + "properties": { + "account_holder_name": { + "type": "string", + "example": "Jane Doe" + }, + "account_number": { + "type": "string", + "example": "10244123908" + }, + "sort_code": { + "type": "string", + "example": "012" + } + } + }, + "Bank": { + "oneOf": [ + { + "$ref": "#/components/schemas/AchBankTransfer" + }, + { + "$ref": "#/components/schemas/BacsBankTransfer" + }, + { + "$ref": "#/components/schemas/SepaBankTransfer" + } + ] + }, + "BankDebitBilling": { + "type": "object", + "required": [ + "name", + "email" + ], + "properties": { + "name": { + "type": "string", + "description": "The billing name for bank debits", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The billing email for bank debits", + "example": "example@example.com" + }, + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + } + } + }, + "BankDebitData": { + "oneOf": [ + { + "type": "object", + "required": [ + "ach_bank_debit" + ], + "properties": { + "ach_bank_debit": { + "type": "object", + "description": "Payment Method data for Ach bank debit", + "required": [ + "billing_details", + "account_number", + "routing_number", + "card_holder_name", + "bank_account_holder_name", + "bank_name", + "bank_type", + "bank_holder_type" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankDebitBilling" + }, + "account_number": { + "type": "string", + "description": "Account number for ach bank debit payment", + "example": "000123456789" + }, + "routing_number": { + "type": "string", + "description": "Routing number for ach bank debit payment", + "example": "110000000" + }, + "card_holder_name": { + "type": "string", + "example": "John Test" + }, + "bank_account_holder_name": { + "type": "string", + "example": "John Doe" + }, + "bank_name": { + "type": "string", + "example": "ACH" + }, + "bank_type": { + "type": "string", + "example": "Checking" + }, + "bank_holder_type": { + "type": "string", + "example": "Personal" + } + } + } + } + }, + { + "type": "object", + "required": [ + "sepa_bank_debit" + ], + "properties": { + "sepa_bank_debit": { + "type": "object", + "required": [ + "billing_details", + "iban", + "bank_account_holder_name" + ], + "properties": { + "billing_details": { + "$ref": "#/components/schemas/BankDebitBilling" + }, + "iban": { + "type": "string", + "description": "International bank account number (iban) for SEPA", + "example": "DE89370400440532013000" + }, + "bank_account_holder_name": { + "type": "string", + "description": "Owner name for bank debit", + "example": "A. Schneider" + } + } + } + } + }, + { + "type": "object", + "required": [ + "becs_bank_debit" ], "properties": { "becs_bank_debit": { @@ -4180,94 +5790,368 @@ "data": { "type": "string" } - } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fingerprint" + ] + }, + "data": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "extended_card_bin" + ] + }, + "data": { + "type": "string" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "BlocklistResponse": { + "type": "object", + "required": [ + "fingerprint_id", + "data_kind", + "created_at" + ], + "properties": { + "fingerprint_id": { + "type": "string" + }, + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BoletoVoucherData": { + "type": "object", + "properties": { + "social_security_number": { + "type": "string", + "description": "The shopper's social security number", + "nullable": true + } + } + }, + "BrowserInformation": { + "type": "object", + "description": "Browser information to be used for 3DS 2.0", + "properties": { + "color_depth": { + "type": "integer", + "format": "int32", + "description": "Color depth supported by the browser", + "nullable": true, + "minimum": 0 + }, + "java_enabled": { + "type": "boolean", + "description": "Whether java is enabled in the browser", + "nullable": true + }, + "java_script_enabled": { + "type": "boolean", + "description": "Whether javascript is enabled in the browser", + "nullable": true + }, + "language": { + "type": "string", + "description": "Language supported", + "nullable": true + }, + "screen_height": { + "type": "integer", + "format": "int32", + "description": "The screen height in pixels", + "nullable": true, + "minimum": 0 + }, + "screen_width": { + "type": "integer", + "format": "int32", + "description": "The screen width in pixels", + "nullable": true, + "minimum": 0 + }, + "time_zone": { + "type": "integer", + "format": "int32", + "description": "Time zone of the client", + "nullable": true + }, + "ip_address": { + "type": "string", + "description": "Ip address of the client", + "nullable": true + }, + "accept_header": { + "type": "string", + "description": "List of headers that are accepted", + "example": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "nullable": true + }, + "user_agent": { + "type": "string", + "description": "User-agent of the browser", + "nullable": true + } + } + }, + "BusinessPaymentLinkConfig": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + }, + { + "type": "object", + "properties": { + "domain_name": { + "type": "string", + "nullable": true + } + } + } + ] + }, + "BusinessProfileCreate": { + "type": "object", + "properties": { + "profile_name": { + "type": "string", + "description": "The name of business profile", + "nullable": true, + "maxLength": 64 + }, + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 + }, + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": true, + "example": true, + "nullable": true + }, + "payment_response_hash_key": { + "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "nullable": true + }, + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true }, - { + "metadata": { "type": "object", - "required": [ - "type", - "data" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "fingerprint" - ] - }, - "data": { - "type": "string" - } - } + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - { + "routing_algorithm": { "type": "object", - "required": [ - "type", - "data" + "description": "The routing algorithm to be used for routing payments to desired connectors", + "nullable": true + }, + "intent_fulfillment_time": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_routing_algorithm": { + "type": "object", + "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "nullable": true + }, + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } ], - "properties": { - "type": { - "type": "string", - "enum": [ - "extended_card_bin" - ] - }, - "data": { - "type": "string" + "nullable": true + }, + "applepay_verified_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified applepay domains for a particular profile", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Client Secret Default expiry for all payments created under this business profile", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/BusinessPaymentLinkConfig" } - } + ], + "nullable": true } - ], - "discriminator": { - "propertyName": "type" } }, - "BlocklistResponse": { + "BusinessProfileResponse": { "type": "object", "required": [ - "fingerprint_id", - "data_kind", - "created_at" + "merchant_id", + "profile_id", + "profile_name", + "enable_payment_response_hash", + "redirect_to_merchant_with_http_post" ], "properties": { - "fingerprint_id": { - "type": "string" + "merchant_id": { + "type": "string", + "description": "The identifier for Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 }, - "data_kind": { - "$ref": "#/components/schemas/BlocklistDataKind" + "profile_id": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments", + "example": "pro_abcdefghijklmnopqrstuvwxyz", + "maxLength": 64 }, - "created_at": { + "profile_name": { "type": "string", - "format": "date-time" - } - } - }, - "BoletoVoucherData": { - "type": "object", - "properties": { - "social_security_number": { + "description": "Name of the business profile", + "maxLength": 64 + }, + "return_url": { "type": "string", - "description": "The shopper's social security number", + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 + }, + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": true, + "example": true + }, + "payment_response_hash_key": { + "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", "nullable": true - } - } - }, - "BusinessPaymentLinkConfig": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentLinkConfigRequest" }, - { + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true + }, + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true + }, + "metadata": { "type": "object", - "properties": { - "domain_name": { - "type": "string", - "nullable": true + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "routing_algorithm": { + "type": "object", + "description": "The routing algorithm to be used for routing payments to desired connectors", + "nullable": true + }, + "intent_fulfillment_time": { + "type": "integer", + "format": "int64", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true + }, + "frm_routing_algorithm": { + "type": "object", + "description": "The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'", + "nullable": true + }, + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" } - } + ], + "nullable": true + }, + "applepay_verified_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified applepay domains for a particular profile", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int64", + "description": "Client Secret Default expiry for all payments created under this business profile", + "example": 900, + "nullable": true + }, + "payment_link_config": { + "description": "Default Payment Link config for all payment links created under this business profile", + "nullable": true } - ] + } }, "CaptureMethod": { "type": "string", @@ -4516,6 +6400,7 @@ }, "CardNetwork": { "type": "string", + "description": "Indicates the card network.", "enum": [ "Visa", "Mastercard", @@ -4599,16 +6484,49 @@ "CashappQr": { "type": "object" }, + "Comparison": { + "type": "object", + "description": "Represents a single comparison condition.", + "required": [ + "lhs", + "comparison", + "value", + "metadata" + ], + "properties": { + "lhs": { + "type": "string", + "description": "The left hand side which will always be a domain input identifier like \"payment.method.cardtype\"" + }, + "comparison": { + "$ref": "#/components/schemas/ComparisonType" + }, + "value": { + "$ref": "#/components/schemas/ValueType" + }, + "metadata": { + "type": "object", + "description": "Additional metadata that the Static Analyzer and Backend does not touch.\nThis can be used to store useful information for the frontend and is required for communication\nbetween the static analyzer and the frontend.", + "additionalProperties": {} + } + } + }, + "ComparisonType": { + "type": "string", + "description": "Conditional comparison type", + "enum": [ + "equal", + "not_equal", + "less_than", + "less_than_equal", + "greater_than", + "greater_than_equal" + ] + }, "Connector": { "type": "string", + "description": "A connector is an integration to fulfill payments", "enum": [ - "phonypay", - "fauxpay", - "pretendpay", - "stripe_test", - "adyen_test", - "checkout_test", - "paypal_test", "aci", "adyen", "airwallex", @@ -4671,25 +6589,74 @@ { "$ref": "#/components/schemas/ApplepayConnectorMetadataRequest" } - ], - "nullable": true + ], + "nullable": true + }, + "airwallex": { + "allOf": [ + { + "$ref": "#/components/schemas/AirwallexData" + } + ], + "nullable": true + }, + "noon": { + "allOf": [ + { + "$ref": "#/components/schemas/NoonData" + } + ], + "nullable": true + } + } + }, + "ConnectorSelection": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } }, - "airwallex": { - "allOf": [ - { - "$ref": "#/components/schemas/AirwallexData" - } + { + "type": "object", + "required": [ + "type", + "data" ], - "nullable": true - }, - "noon": { - "allOf": [ - { - "$ref": "#/components/schemas/NoonData" + "properties": { + "type": { + "type": "string", + "enum": [ + "volume_split" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorVolumeSplit" + } } - ], - "nullable": true + } } + ], + "discriminator": { + "propertyName": "type" } }, "ConnectorStatus": { @@ -4701,6 +6668,7 @@ }, "ConnectorType": { "type": "string", + "description": "Type of the Connector for the financial use case. Could range from Payments to Accounting to Banking.", "enum": [ "payment_processor", "payment_vas", @@ -4713,6 +6681,23 @@ "payment_method_auth" ] }, + "ConnectorVolumeSplit": { + "type": "object", + "required": [ + "connector", + "split" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + }, + "split": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, "CountryAlpha2": { "type": "string", "enum": [ @@ -4967,4735 +6952,5685 @@ "US" ] }, - "CreateApiKeyRequest": { + "CreateApiKeyRequest": { + "type": "object", + "description": "The request body for creating an API Key.", + "required": [ + "name", + "expiration" + ], + "properties": { + "name": { + "type": "string", + "description": "A unique name for the API Key to help you identify it.", + "example": "Sandbox integration key", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "A description to provide more context about the API Key.", + "example": "Key used by our developers to integrate with the sandbox environment", + "nullable": true, + "maxLength": 256 + }, + "expiration": { + "$ref": "#/components/schemas/ApiKeyExpiration" + } + } + }, + "CreateApiKeyResponse": { + "type": "object", + "description": "The response body for creating an API Key.", + "required": [ + "key_id", + "merchant_id", + "name", + "api_key", + "created", + "expiration" + ], + "properties": { + "key_id": { + "type": "string", + "description": "The identifier for the API Key.", + "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", + "maxLength": 64 + }, + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account.", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 + }, + "name": { + "type": "string", + "description": "The unique name for the API Key to help you identify it.", + "example": "Sandbox integration key", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "The description to provide more context about the API Key.", + "example": "Key used by our developers to integrate with the sandbox environment", + "nullable": true, + "maxLength": 256 + }, + "api_key": { + "type": "string", + "description": "The plaintext API Key used for server-side API access. Ensure you store the API Key\nsecurely as you will not be able to see it again.", + "maxLength": 128 + }, + "created": { + "type": "string", + "format": "date-time", + "description": "The time at which the API Key was created.", + "example": "2022-09-10T10:11:12Z" + }, + "expiration": { + "$ref": "#/components/schemas/ApiKeyExpiration" + } + } + }, + "CryptoData": { + "type": "object", + "properties": { + "pay_currency": { + "type": "string", + "nullable": true + } + } + }, + "Currency": { + "type": "string", + "description": "The three letter ISO currency code in uppercase. Eg: 'USD' for the United States Dollar.", + "enum": [ + "AED", + "ALL", + "AMD", + "ANG", + "ARS", + "AUD", + "AWG", + "AZN", + "BBD", + "BDT", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BWP", + "BZD", + "CAD", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ETB", + "EUR", + "FJD", + "GBP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RUB", + "RWF", + "SAR", + "SCR", + "SEK", + "SGD", + "SLL", + "SOS", + "SSP", + "SVC", + "SZL", + "THB", + "TRY", + "TTD", + "TWD", + "TZS", + "UGX", + "USD", + "UYU", + "UZS", + "VND", + "VUV", + "XAF", + "XOF", + "XPF", + "YER", + "ZAR" + ] + }, + "CustomerAcceptance": { "type": "object", - "description": "The request body for creating an API Key.", "required": [ - "name", - "expiration" + "acceptance_type" + ], + "properties": { + "acceptance_type": { + "$ref": "#/components/schemas/AcceptanceType" + }, + "accepted_at": { + "type": "string", + "format": "date-time", + "description": "Specifying when the customer acceptance was provided", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "online": { + "allOf": [ + { + "$ref": "#/components/schemas/OnlineMandate" + } + ], + "nullable": true + } + } + }, + "CustomerDeleteResponse": { + "type": "object", + "required": [ + "customer_id", + "customer_deleted", + "address_deleted", + "payment_methods_deleted" + ], + "properties": { + "customer_id": { + "type": "string", + "description": "The identifier for the customer object", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 + }, + "customer_deleted": { + "type": "boolean", + "description": "Whether customer was deleted or not", + "example": false + }, + "address_deleted": { + "type": "boolean", + "description": "Whether address was deleted or not", + "example": false + }, + "payment_methods_deleted": { + "type": "boolean", + "description": "Whether payment methods deleted or not", + "example": false + } + } + }, + "CustomerDetails": { + "type": "object", + "required": [ + "id" ], "properties": { + "id": { + "type": "string", + "description": "The identifier for the customer." + }, "name": { "type": "string", - "description": "A unique name for the API Key to help you identify it.", - "example": "Sandbox integration key", - "maxLength": 64 + "description": "The customer's name", + "example": "John Doe", + "nullable": true, + "maxLength": 255 }, - "description": { + "email": { "type": "string", - "description": "A description to provide more context about the API Key.", - "example": "Key used by our developers to integrate with the sandbox environment", + "description": "The customer's email address", + "example": "johntest@test.com", "nullable": true, - "maxLength": 256 + "maxLength": 255 }, - "expiration": { - "$ref": "#/components/schemas/ApiKeyExpiration" + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "3141592653", + "nullable": true, + "maxLength": 10 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer's phone number", + "example": "+1", + "nullable": true, + "maxLength": 2 } } }, - "CreateApiKeyResponse": { + "CustomerPaymentMethod": { "type": "object", - "description": "The response body for creating an API Key.", "required": [ - "key_id", - "merchant_id", - "name", - "api_key", - "created", - "expiration" + "payment_token", + "customer_id", + "payment_method", + "recurring_enabled", + "installment_payment_enabled", + "requires_cvv" ], "properties": { - "key_id": { + "payment_token": { "type": "string", - "description": "The identifier for the API Key.", - "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", - "maxLength": 64 + "description": "Token for payment method in temporary card locker which gets refreshed often", + "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" }, - "merchant_id": { + "customer_id": { "type": "string", - "description": "The identifier for the Merchant Account.", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64 + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw" }, - "name": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + }, + "payment_method_issuer": { "type": "string", - "description": "The unique name for the API Key to help you identify it.", - "example": "Sandbox integration key", - "maxLength": 64 + "description": "The name of the bank/ provider issuing the payment method to the end user", + "example": "Citibank", + "nullable": true + }, + "payment_method_issuer_code": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodIssuerCode" + } + ], + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for recurring payments", + "example": true + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for installment payments", + "example": true + }, + "payment_experience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentExperience" + }, + "description": "Type of payment experience enabled with the connector", + "example": [ + "redirect_to_url" + ], + "nullable": true + }, + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetailFromLocker" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "example": "2023-01-18T11:04:09.922Z", + "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } + ], + "nullable": true + }, + "bank": { + "allOf": [ + { + "$ref": "#/components/schemas/MaskedBankDetails" + } + ], + "nullable": true + }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, + "requires_cvv": { + "type": "boolean", + "description": "Whether this payment method requires CVV to be collected", + "example": true + } + } + }, + "CustomerPaymentMethodsListResponse": { + "type": "object", + "required": [ + "customer_payment_methods" + ], + "properties": { + "customer_payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerPaymentMethod" + }, + "description": "List of payment methods for customer" + } + } + }, + "CustomerRequest": { + "type": "object", + "description": "The customer details", + "properties": { + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "description": { + "name": { "type": "string", - "description": "The description to provide more context about the API Key.", - "example": "Key used by our developers to integrate with the sandbox environment", + "description": "The customer's name", + "example": "Jon Test", "nullable": true, - "maxLength": 256 + "maxLength": 255 }, - "api_key": { + "email": { "type": "string", - "description": "The plaintext API Key used for server-side API access. Ensure you store the API Key\nsecurely as you will not be able to see it again.", - "maxLength": 128 + "description": "The customer's email address", + "example": "JonTest@test.com", + "nullable": true, + "maxLength": 255 }, - "created": { + "phone": { "type": "string", - "format": "date-time", - "description": "The time at which the API Key was created.", - "example": "2022-09-10T10:11:12Z" + "description": "The customer's phone number", + "example": "9999999999", + "nullable": true, + "maxLength": 255 }, - "expiration": { - "$ref": "#/components/schemas/ApiKeyExpiration" - } - } - }, - "CryptoData": { - "type": "object", - "properties": { - "pay_currency": { + "description": { "type": "string", - "nullable": true - } - } - }, - "Currency": { - "type": "string", - "enum": [ - "AED", - "ALL", - "AMD", - "ANG", - "ARS", - "AUD", - "AWG", - "AZN", - "BBD", - "BDT", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BWP", - "BZD", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CUP", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ETB", - "EUR", - "FJD", - "GBP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RUB", - "RWF", - "SAR", - "SCR", - "SEK", - "SGD", - "SLL", - "SOS", - "SSP", - "SVC", - "SZL", - "THB", - "TRY", - "TTD", - "TWD", - "TZS", - "UGX", - "USD", - "UYU", - "UZS", - "VND", - "VUV", - "XAF", - "XOF", - "XPF", - "YER", - "ZAR" - ] - }, - "CustomerAcceptance": { - "type": "object", - "required": [ - "acceptance_type" - ], - "properties": { - "acceptance_type": { - "$ref": "#/components/schemas/AcceptanceType" + "description": "An arbitrary string that you can attach to a customer object.", + "example": "First Customer", + "nullable": true, + "maxLength": 255 }, - "accepted_at": { + "phone_country_code": { "type": "string", - "format": "date-time", - "description": "Specifying when the customer acceptance was provided", - "example": "2022-09-10T10:11:12Z", - "nullable": true + "description": "The country code for the customer phone number", + "example": "+65", + "nullable": true, + "maxLength": 255 }, - "online": { + "address": { "allOf": [ { - "$ref": "#/components/schemas/OnlineMandate" + "$ref": "#/components/schemas/AddressDetails" } ], "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "nullable": true } } }, - "CustomerDeleteResponse": { + "CustomerResponse": { "type": "object", "required": [ "customer_id", - "customer_deleted", - "address_deleted", - "payment_methods_deleted" + "created_at" ], "properties": { "customer_id": { "type": "string", - "description": "The identifier for the customer object", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", "maxLength": 255 }, - "customer_deleted": { - "type": "boolean", - "description": "Whether customer was deleted or not", - "example": false + "name": { + "type": "string", + "description": "The customer's name", + "example": "Jon Test", + "nullable": true, + "maxLength": 255 }, - "address_deleted": { - "type": "boolean", - "description": "Whether address was deleted or not", - "example": false + "email": { + "type": "string", + "description": "The customer's email address", + "example": "JonTest@test.com", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "9999999999", + "nullable": true, + "maxLength": 255 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number", + "example": "+65", + "nullable": true, + "maxLength": 255 + }, + "description": { + "type": "string", + "description": "An arbitrary string that you can attach to a customer object.", + "example": "First Customer", + "nullable": true, + "maxLength": 255 + }, + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "example": "2023-01-18T11:04:09.922Z" }, - "payment_methods_deleted": { - "type": "boolean", - "description": "Whether payment methods deleted or not", - "example": false + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "nullable": true } } }, - "CustomerDetails": { + "DisputeResponse": { "type": "object", "required": [ - "id" + "dispute_id", + "payment_id", + "attempt_id", + "amount", + "currency", + "dispute_stage", + "dispute_status", + "connector", + "connector_status", + "connector_dispute_id", + "created_at" ], "properties": { - "id": { + "dispute_id": { "type": "string", - "description": "The identifier for the customer." + "description": "The identifier for dispute" }, - "name": { + "payment_id": { "type": "string", - "description": "The customer's name", - "example": "John Doe", - "nullable": true, - "maxLength": 255 + "description": "The identifier for payment_intent" }, - "email": { + "attempt_id": { "type": "string", - "description": "The customer's email address", - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 + "description": "The identifier for payment_attempt" }, - "phone": { + "amount": { "type": "string", - "description": "The customer's phone number", - "example": "3141592653", - "nullable": true, - "maxLength": 10 + "description": "The dispute amount" }, - "phone_country_code": { + "currency": { "type": "string", - "description": "The country code for the customer's phone number", - "example": "+1", - "nullable": true, - "maxLength": 2 - } - } - }, - "CustomerPaymentMethod": { - "type": "object", - "required": [ - "payment_token", - "customer_id", - "payment_method", - "recurring_enabled", - "installment_payment_enabled", - "requires_cvv" - ], - "properties": { - "payment_token": { + "description": "The three-letter ISO currency code" + }, + "dispute_stage": { + "$ref": "#/components/schemas/DisputeStage" + }, + "dispute_status": { + "$ref": "#/components/schemas/DisputeStatus" + }, + "connector": { "type": "string", - "description": "Token for payment method in temporary card locker which gets refreshed often", - "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" + "description": "connector to which dispute is associated with" }, - "customer_id": { + "connector_status": { "type": "string", - "description": "The unique identifier of the customer.", - "example": "cus_meowerunwiuwiwqw" + "description": "Status of the dispute sent by connector" }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" + "connector_dispute_id": { + "type": "string", + "description": "Dispute id sent by connector" }, - "payment_method_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodType" - } - ], + "connector_reason": { + "type": "string", + "description": "Reason of dispute sent by connector", "nullable": true }, - "payment_method_issuer": { + "connector_reason_code": { "type": "string", - "description": "The name of the bank/ provider issuing the payment method to the end user", - "example": "Citibank", + "description": "Reason code of dispute sent by connector", "nullable": true }, - "payment_method_issuer_code": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodIssuerCode" - } - ], + "challenge_required_by": { + "type": "string", + "format": "date-time", + "description": "Evidence deadline of dispute sent by connector", "nullable": true }, - "recurring_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for recurring payments", - "example": true - }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for installment payments", - "example": true - }, - "payment_experience": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentExperience" - }, - "description": "Type of payment experience enabled with the connector", - "example": [ - "redirect_to_url" - ], + "connector_created_at": { + "type": "string", + "format": "date-time", + "description": "Dispute created time sent by connector", "nullable": true }, - "card": { - "allOf": [ - { - "$ref": "#/components/schemas/CardDetailFromLocker" - } - ], + "connector_updated_at": { + "type": "string", + "format": "date-time", + "description": "Dispute updated time sent by connector", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "created_at": { + "type": "string", + "format": "date-time", + "description": "Time at which dispute is received" + } + } + }, + "DisputeResponsePaymentsRetrieve": { + "type": "object", + "required": [ + "dispute_id", + "dispute_stage", + "dispute_status", + "connector_status", + "connector_dispute_id", + "created_at" + ], + "properties": { + "dispute_id": { + "type": "string", + "description": "The identifier for dispute" + }, + "dispute_stage": { + "$ref": "#/components/schemas/DisputeStage" + }, + "dispute_status": { + "$ref": "#/components/schemas/DisputeStatus" + }, + "connector_status": { + "type": "string", + "description": "Status of the dispute sent by connector" + }, + "connector_dispute_id": { + "type": "string", + "description": "Dispute id sent by connector" + }, + "connector_reason": { + "type": "string", + "description": "Reason of dispute sent by connector", "nullable": true }, - "created": { + "connector_reason_code": { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z", + "description": "Reason code of dispute sent by connector", "nullable": true }, - "bank_transfer": { - "allOf": [ - { - "$ref": "#/components/schemas/Bank" - } - ], + "challenge_required_by": { + "type": "string", + "format": "date-time", + "description": "Evidence deadline of dispute sent by connector", "nullable": true }, - "bank": { - "allOf": [ - { - "$ref": "#/components/schemas/MaskedBankDetails" - } - ], + "connector_created_at": { + "type": "string", + "format": "date-time", + "description": "Dispute created time sent by connector", "nullable": true }, - "surcharge_details": { - "allOf": [ - { - "$ref": "#/components/schemas/SurchargeDetailsResponse" - } - ], + "connector_updated_at": { + "type": "string", + "format": "date-time", + "description": "Dispute updated time sent by connector", "nullable": true }, - "requires_cvv": { - "type": "boolean", - "description": "Whether this payment method requires CVV to be collected", - "example": true + "created_at": { + "type": "string", + "format": "date-time", + "description": "Time at which dispute is received" } } }, - "CustomerPaymentMethodsListResponse": { + "DisputeStage": { + "type": "string", + "enum": [ + "pre_dispute", + "dispute", + "pre_arbitration" + ] + }, + "DisputeStatus": { + "type": "string", + "enum": [ + "dispute_opened", + "dispute_expired", + "dispute_accepted", + "dispute_cancelled", + "dispute_challenged", + "dispute_won", + "dispute_lost" + ] + }, + "DokuBankTransferInstructions": { "type": "object", "required": [ - "customer_payment_methods" + "expires_at", + "reference", + "instructions_url" ], "properties": { - "customer_payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerPaymentMethod" - }, - "description": "List of payment methods for customer" + "expires_at": { + "type": "string", + "example": "2023-07-26T17:33:00-07-21" + }, + "reference": { + "type": "string", + "example": "122385736258" + }, + "instructions_url": { + "type": "string" } } }, - "CustomerRequest": { + "DokuBillingDetails": { "type": "object", - "description": "The customer details", + "required": [ + "first_name", + "last_name", + "email" + ], "properties": { - "customer_id": { + "first_name": { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "description": "The billing first name for Doku", + "example": "Jane" }, - "name": { + "last_name": { "type": "string", - "description": "The customer's name", - "example": "Jon Test", - "nullable": true, - "maxLength": 255 + "description": "The billing second name for Doku", + "example": "Doe" }, "email": { "type": "string", - "description": "The customer's email address", - "example": "JonTest@test.com", - "nullable": true, - "maxLength": 255 - }, - "phone": { + "description": "The Email ID for Doku billing", + "example": "example@me.com" + } + } + }, + "EphemeralKeyCreateResponse": { + "type": "object", + "required": [ + "customer_id", + "created_at", + "expires", + "secret" + ], + "properties": { + "customer_id": { "type": "string", - "description": "The customer's phone number", - "example": "9999999999", - "nullable": true, - "maxLength": 255 + "description": "customer_id to which this ephemeral key belongs to" }, - "description": { - "type": "string", - "description": "An arbitrary string that you can attach to a customer object.", - "example": "First Customer", - "nullable": true, - "maxLength": 255 + "created_at": { + "type": "integer", + "format": "int64", + "description": "time at which this ephemeral key was created" }, - "phone_country_code": { - "type": "string", - "description": "The country code for the customer phone number", - "example": "+65", - "nullable": true, - "maxLength": 255 + "expires": { + "type": "integer", + "format": "int64", + "description": "time at which this ephemeral key would expire" }, - "address": { + "secret": { + "type": "string", + "description": "ephemeral key" + } + } + }, + "EventType": { + "type": "string", + "enum": [ + "payment_succeeded", + "payment_failed", + "payment_processing", + "payment_cancelled", + "payment_authorized", + "payment_captured", + "action_required", + "refund_succeeded", + "refund_failed", + "dispute_opened", + "dispute_expired", + "dispute_accepted", + "dispute_cancelled", + "dispute_challenged", + "dispute_won", + "dispute_lost", + "mandate_active", + "mandate_revoked" + ] + }, + "FeatureMetadata": { + "type": "object", + "properties": { + "redirect_response": { "allOf": [ { - "$ref": "#/components/schemas/AddressDetails" + "$ref": "#/components/schemas/RedirectResponse" } ], "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", - "nullable": true } } }, - "CustomerResponse": { - "type": "object", - "required": [ - "customer_id", - "created_at" - ], - "properties": { - "customer_id": { + "FieldType": { + "oneOf": [ + { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "enum": [ + "user_card_number" + ] }, - "name": { + { "type": "string", - "description": "The customer's name", - "example": "Jon Test", - "nullable": true, - "maxLength": 255 + "enum": [ + "user_card_expiry_month" + ] }, - "email": { + { "type": "string", - "description": "The customer's email address", - "example": "JonTest@test.com", - "nullable": true, - "maxLength": 255 + "enum": [ + "user_card_expiry_year" + ] }, - "phone": { + { "type": "string", - "description": "The customer's phone number", - "example": "9999999999", - "nullable": true, - "maxLength": 255 + "enum": [ + "user_card_cvc" + ] }, - "phone_country_code": { + { "type": "string", - "description": "The country code for the customer phone number", - "example": "+65", - "nullable": true, - "maxLength": 255 + "enum": [ + "user_full_name" + ] }, - "description": { + { "type": "string", - "description": "An arbitrary string that you can attach to a customer object.", - "example": "First Customer", - "nullable": true, - "maxLength": 255 + "enum": [ + "user_email_address" + ] }, - "address": { - "allOf": [ - { - "$ref": "#/components/schemas/AddressDetails" - } - ], - "nullable": true + { + "type": "string", + "enum": [ + "user_phone_number" + ] }, - "created_at": { + { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z" + "enum": [ + "user_country_code" + ] }, - "metadata": { + { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", - "nullable": true - } - } - }, - "DisputeResponse": { - "type": "object", - "required": [ - "dispute_id", - "payment_id", - "attempt_id", - "amount", - "currency", - "dispute_stage", - "dispute_status", - "connector", - "connector_status", - "connector_dispute_id", - "created_at" - ], - "properties": { - "dispute_id": { - "type": "string", - "description": "The identifier for dispute" + "required": [ + "user_country" + ], + "properties": { + "user_country": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } }, - "payment_id": { - "type": "string", - "description": "The identifier for payment_intent" + { + "type": "object", + "required": [ + "user_currency" + ], + "properties": { + "user_currency": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } }, - "attempt_id": { + { "type": "string", - "description": "The identifier for payment_attempt" + "enum": [ + "user_billing_name" + ] }, - "amount": { + { "type": "string", - "description": "The dispute amount" + "enum": [ + "user_address_line1" + ] }, - "currency": { + { "type": "string", - "description": "The three-letter ISO currency code" - }, - "dispute_stage": { - "$ref": "#/components/schemas/DisputeStage" - }, - "dispute_status": { - "$ref": "#/components/schemas/DisputeStatus" + "enum": [ + "user_address_line2" + ] }, - "connector": { + { "type": "string", - "description": "connector to which dispute is associated with" + "enum": [ + "user_address_city" + ] }, - "connector_status": { + { "type": "string", - "description": "Status of the dispute sent by connector" + "enum": [ + "user_address_pincode" + ] }, - "connector_dispute_id": { + { "type": "string", - "description": "Dispute id sent by connector" + "enum": [ + "user_address_state" + ] }, - "connector_reason": { - "type": "string", - "description": "Reason of dispute sent by connector", - "nullable": true + { + "type": "object", + "required": [ + "user_address_country" + ], + "properties": { + "user_address_country": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } }, - "connector_reason_code": { + { "type": "string", - "description": "Reason code of dispute sent by connector", - "nullable": true + "enum": [ + "user_blik_code" + ] }, - "challenge_required_by": { + { "type": "string", - "format": "date-time", - "description": "Evidence deadline of dispute sent by connector", - "nullable": true + "enum": [ + "user_bank" + ] }, - "connector_created_at": { + { "type": "string", - "format": "date-time", - "description": "Dispute created time sent by connector", - "nullable": true + "enum": [ + "text" + ] }, - "connector_updated_at": { - "type": "string", - "format": "date-time", - "description": "Dispute updated time sent by connector", - "nullable": true + { + "type": "object", + "required": [ + "drop_down" + ], + "properties": { + "drop_down": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + ], + "description": "Possible field type of required fields in payment_method_data" + }, + "FrmAction": { + "type": "string", + "enum": [ + "cancel_txn", + "auto_refund", + "manual_review" + ] + }, + "FrmConfigs": { + "type": "object", + "description": "Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", + "required": [ + "gateway", + "payment_methods" + ], + "properties": { + "gateway": { + "$ref": "#/components/schemas/ConnectorType" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "Time at which dispute is received" + "payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmPaymentMethod" + }, + "description": "payment methods that can be used in the payment" } } }, - "DisputeResponsePaymentsRetrieve": { + "FrmMessage": { "type": "object", + "description": "frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None", "required": [ - "dispute_id", - "dispute_stage", - "dispute_status", - "connector_status", - "connector_dispute_id", - "created_at" + "frm_name" ], "properties": { - "dispute_id": { - "type": "string", - "description": "The identifier for dispute" - }, - "dispute_stage": { - "$ref": "#/components/schemas/DisputeStage" - }, - "dispute_status": { - "$ref": "#/components/schemas/DisputeStatus" + "frm_name": { + "type": "string" }, - "connector_status": { + "frm_transaction_id": { "type": "string", - "description": "Status of the dispute sent by connector" + "nullable": true }, - "connector_dispute_id": { + "frm_transaction_type": { "type": "string", - "description": "Dispute id sent by connector" + "nullable": true }, - "connector_reason": { + "frm_status": { "type": "string", - "description": "Reason of dispute sent by connector", "nullable": true }, - "connector_reason_code": { - "type": "string", - "description": "Reason code of dispute sent by connector", + "frm_score": { + "type": "integer", + "format": "int32", "nullable": true }, - "challenge_required_by": { - "type": "string", - "format": "date-time", - "description": "Evidence deadline of dispute sent by connector", + "frm_reason": { "nullable": true }, - "connector_created_at": { + "frm_error": { "type": "string", - "format": "date-time", - "description": "Dispute created time sent by connector", "nullable": true + } + } + }, + "FrmPaymentMethod": { + "type": "object", + "description": "Details of FrmPaymentMethod are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", + "required": [ + "payment_method", + "payment_method_types" + ], + "properties": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmPaymentMethodType" + }, + "description": "payment method types(credit, debit) that can be used in the payment" + } + } + }, + "FrmPaymentMethodType": { + "type": "object", + "description": "Details of FrmPaymentMethodType are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", + "required": [ + "payment_method_type", + "card_networks", + "flow", + "action" + ], + "properties": { + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" }, - "connector_updated_at": { - "type": "string", - "format": "date-time", - "description": "Dispute updated time sent by connector", - "nullable": true + "card_networks": { + "$ref": "#/components/schemas/CardNetwork" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "Time at which dispute is received" + "flow": { + "$ref": "#/components/schemas/FrmPreferredFlowTypes" + }, + "action": { + "$ref": "#/components/schemas/FrmAction" } } }, - "DisputeStage": { + "FrmPreferredFlowTypes": { "type": "string", "enum": [ - "pre_dispute", - "dispute", - "pre_arbitration" + "pre", + "post" ] }, - "DisputeStatus": { + "FutureUsage": { "type": "string", "enum": [ - "dispute_opened", - "dispute_expired", - "dispute_accepted", - "dispute_cancelled", - "dispute_challenged", - "dispute_won", - "dispute_lost" + "off_session", + "on_session" ] }, - "DokuBankTransferInstructions": { - "type": "object", - "required": [ - "expires_at", - "reference", - "instructions_url" - ], - "properties": { - "expires_at": { - "type": "string", - "example": "2023-07-26T17:33:00-07-21" - }, - "reference": { - "type": "string", - "example": "122385736258" + "GcashRedirection": { + "type": "object" + }, + "GiftCardData": { + "oneOf": [ + { + "type": "object", + "required": [ + "givex" + ], + "properties": { + "givex": { + "$ref": "#/components/schemas/GiftCardDetails" + } + } }, - "instructions_url": { - "type": "string" + { + "type": "object", + "required": [ + "pay_safe_card" + ], + "properties": { + "pay_safe_card": { + "type": "object" + } + } } - } + ] }, - "DokuBillingDetails": { + "GiftCardDetails": { "type": "object", "required": [ - "first_name", - "last_name", - "email" + "number", + "cvc" ], "properties": { - "first_name": { - "type": "string", - "description": "The billing first name for Doku", - "example": "Jane" - }, - "last_name": { + "number": { "type": "string", - "description": "The billing second name for Doku", - "example": "Doe" + "description": "The gift card number" }, - "email": { + "cvc": { "type": "string", - "description": "The Email ID for Doku billing", - "example": "example@me.com" + "description": "The card verification code." } } }, - "EphemeralKeyCreateResponse": { + "GoPayRedirection": { + "type": "object" + }, + "GooglePayPaymentMethodInfo": { "type": "object", "required": [ - "customer_id", - "created_at", - "expires", - "secret" + "card_network", + "card_details" ], "properties": { - "customer_id": { + "card_network": { "type": "string", - "description": "customer_id to which this ephemeral key belongs to" - }, - "created_at": { - "type": "integer", - "format": "int64", - "description": "time at which this ephemeral key was created" - }, - "expires": { - "type": "integer", - "format": "int64", - "description": "time at which this ephemeral key would expire" + "description": "The name of the card network" }, - "secret": { + "card_details": { "type": "string", - "description": "ephemeral key" + "description": "The details of the card" } } }, - "EventType": { - "type": "string", - "enum": [ - "payment_succeeded", - "payment_failed", - "payment_processing", - "payment_cancelled", - "payment_authorized", - "payment_captured", - "action_required", - "refund_succeeded", - "refund_failed", - "dispute_opened", - "dispute_expired", - "dispute_accepted", - "dispute_cancelled", - "dispute_challenged", - "dispute_won", - "dispute_lost", - "mandate_active", - "mandate_revoked" - ] + "GooglePayRedirectData": { + "type": "object" }, - "FeatureMetadata": { + "GooglePaySessionResponse": { "type": "object", + "required": [ + "merchant_info", + "allowed_payment_methods", + "transaction_info", + "delayed_session_token", + "connector", + "sdk_next_action" + ], "properties": { - "redirect_response": { - "allOf": [ - { - "$ref": "#/components/schemas/RedirectResponse" - } - ], - "nullable": true - } - } - }, - "FieldType": { - "oneOf": [ - { - "type": "string", - "enum": [ - "user_card_number" - ] - }, - { - "type": "string", - "enum": [ - "user_card_expiry_month" - ] - }, - { - "type": "string", - "enum": [ - "user_card_expiry_year" - ] - }, - { - "type": "string", - "enum": [ - "user_card_cvc" - ] + "merchant_info": { + "$ref": "#/components/schemas/GpayMerchantInfo" }, - { - "type": "string", - "enum": [ - "user_full_name" - ] + "allowed_payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GpayAllowedPaymentMethods" + }, + "description": "List of the allowed payment meythods" }, - { - "type": "string", - "enum": [ - "user_email_address" - ] + "transaction_info": { + "$ref": "#/components/schemas/GpayTransactionInfo" }, - { - "type": "string", - "enum": [ - "user_phone_number" - ] + "delayed_session_token": { + "type": "boolean", + "description": "Identifier for the delayed session response" }, - { + "connector": { "type": "string", - "enum": [ - "user_country_code" - ] + "description": "The name of the connector" }, - { - "type": "object", - "required": [ - "user_country" - ], - "properties": { - "user_country": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" }, - { - "type": "object", - "required": [ - "user_currency" - ], - "properties": { - "user_currency": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } + "secrets": { + "allOf": [ + { + "$ref": "#/components/schemas/SecretInfoToInitiateSdk" } - } + ], + "nullable": true + } + } + }, + "GooglePayThirdPartySdk": { + "type": "object", + "required": [ + "delayed_session_token", + "connector", + "sdk_next_action" + ], + "properties": { + "delayed_session_token": { + "type": "boolean", + "description": "Identifier for the delayed session response" }, - { + "connector": { "type": "string", - "enum": [ - "user_billing_name" - ] + "description": "The name of the connector" }, - { + "sdk_next_action": { + "$ref": "#/components/schemas/SdkNextAction" + } + } + }, + "GooglePayThirdPartySdkData": { + "type": "object" + }, + "GooglePayWalletData": { + "type": "object", + "required": [ + "type", + "description", + "info", + "tokenization_data" + ], + "properties": { + "type": { "type": "string", - "enum": [ - "user_address_line1" - ] + "description": "The type of payment method" }, - { + "description": { "type": "string", - "enum": [ - "user_address_line2" - ] + "description": "User-facing message to describe the payment method that funds this transaction." }, - { - "type": "string", - "enum": [ - "user_address_city" - ] + "info": { + "$ref": "#/components/schemas/GooglePayPaymentMethodInfo" }, - { + "tokenization_data": { + "$ref": "#/components/schemas/GpayTokenizationData" + } + } + }, + "GpayAllowedMethodsParameters": { + "type": "object", + "required": [ + "allowed_auth_methods", + "allowed_card_networks" + ], + "properties": { + "allowed_auth_methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of allowed auth methods (ex: 3DS, No3DS, PAN_ONLY etc)" + }, + "allowed_card_networks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of allowed card networks (ex: AMEX,JCB etc)" + } + } + }, + "GpayAllowedPaymentMethods": { + "type": "object", + "required": [ + "type", + "parameters", + "tokenization_specification" + ], + "properties": { + "type": { "type": "string", - "enum": [ - "user_address_pincode" - ] + "description": "The type of payment method" }, - { + "parameters": { + "$ref": "#/components/schemas/GpayAllowedMethodsParameters" + }, + "tokenization_specification": { + "$ref": "#/components/schemas/GpayTokenizationSpecification" + } + } + }, + "GpayMerchantInfo": { + "type": "object", + "required": [ + "merchant_name" + ], + "properties": { + "merchant_id": { "type": "string", - "enum": [ - "user_address_state" - ] + "description": "The merchant Identifier that needs to be passed while invoking Gpay SDK", + "nullable": true }, + "merchant_name": { + "type": "string", + "description": "The name of the merchant that needs to be displayed on Gpay PopUp" + } + } + }, + "GpaySessionTokenResponse": { + "oneOf": [ { - "type": "object", - "required": [ - "user_address_country" - ], - "properties": { - "user_address_country": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "$ref": "#/components/schemas/GooglePayThirdPartySdk" }, { + "$ref": "#/components/schemas/GooglePaySessionResponse" + } + ] + }, + "GpayTokenParameters": { + "type": "object", + "required": [ + "gateway" + ], + "properties": { + "gateway": { "type": "string", - "enum": [ - "user_blik_code" - ] + "description": "The name of the connector" }, - { + "gateway_merchant_id": { "type": "string", - "enum": [ - "user_bank" - ] + "description": "The merchant ID registered in the connector associated", + "nullable": true }, - { + "stripe:version": { "type": "string", - "enum": [ - "text" - ] + "nullable": true }, - { - "type": "object", - "required": [ - "drop_down" - ], - "properties": { - "drop_down": { - "type": "object", - "required": [ - "options" - ], - "properties": { - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "stripe:publishableKey": { + "type": "string", + "nullable": true } + } + }, + "GpayTokenizationData": { + "type": "object", + "required": [ + "type", + "token" ], - "description": "Possible field type of required fields in payment_method_data" + "properties": { + "type": { + "type": "string", + "description": "The type of the token" + }, + "token": { + "type": "string", + "description": "Token generated for the wallet" + } + } }, - "FrmAction": { - "type": "string", - "enum": [ - "cancel_txn", - "auto_refund", - "manual_review" - ] + "GpayTokenizationSpecification": { + "type": "object", + "required": [ + "type", + "parameters" + ], + "properties": { + "type": { + "type": "string", + "description": "The token specification type(ex: PAYMENT_GATEWAY)" + }, + "parameters": { + "$ref": "#/components/schemas/GpayTokenParameters" + } + } }, - "FrmConfigs": { + "GpayTransactionInfo": { "type": "object", - "description": "Details of FrmConfigs are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", "required": [ - "gateway", - "payment_methods" + "country_code", + "currency_code", + "total_price_status", + "total_price" ], "properties": { - "gateway": { - "$ref": "#/components/schemas/ConnectorType" + "country_code": { + "$ref": "#/components/schemas/CountryAlpha2" }, - "payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmPaymentMethod" - }, - "description": "payment methods that can be used in the payment" + "currency_code": { + "$ref": "#/components/schemas/Currency" + }, + "total_price_status": { + "type": "string", + "description": "The total price status (ex: 'FINAL')" + }, + "total_price": { + "type": "string", + "description": "The total price" } } }, - "FrmMessage": { + "GsmCreateRequest": { "type": "object", - "description": "frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None", "required": [ - "frm_name" + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" ], "properties": { - "frm_name": { - "type": "string" + "connector": { + "$ref": "#/components/schemas/Connector" }, - "frm_transaction_id": { + "flow": { "type": "string", - "nullable": true + "description": "The flow in which the code and message occurred for a connector" }, - "frm_transaction_type": { + "sub_flow": { "type": "string", - "nullable": true + "description": "The sub_flow in which the code and message occurred for a connector" }, - "frm_status": { + "code": { "type": "string", - "nullable": true + "description": "code received from the connector" }, - "frm_score": { - "type": "integer", - "format": "int32", + "message": { + "type": "string", + "description": "message received from the connector" + }, + "status": { + "type": "string", + "description": "status provided by the router" + }, + "router_error": { + "type": "string", + "description": "optional error provided by the router", "nullable": true }, - "frm_reason": { + "decision": { + "$ref": "#/components/schemas/GsmDecision" + }, + "step_up_possible": { + "type": "boolean", + "description": "indicates if step_up retry is possible" + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors", "nullable": true }, - "frm_error": { + "unified_message": { "type": "string", + "description": "error message unified across the connectors", "nullable": true } } }, - "FrmPaymentMethod": { + "GsmDecision": { + "type": "string", + "enum": [ + "retry", + "requeue", + "do_default" + ] + }, + "GsmDeleteRequest": { "type": "object", - "description": "Details of FrmPaymentMethod are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", "required": [ - "payment_method", - "payment_method_types" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" + "connector": { + "type": "string", + "description": "The connector through which payment has gone through" }, - "payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmPaymentMethodType" - }, - "description": "payment method types(credit, debit) that can be used in the payment" + "flow": { + "type": "string", + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + }, + "message": { + "type": "string", + "description": "message received from the connector" } } }, - "FrmPaymentMethodType": { + "GsmDeleteResponse": { "type": "object", - "description": "Details of FrmPaymentMethodType are mentioned here... it should be passed in payment connector create api call, and stored in merchant_connector_table", "required": [ - "payment_method_type", - "card_networks", + "gsm_rule_delete", + "connector", "flow", - "action" + "sub_flow", + "code" ], "properties": { - "payment_method_type": { - "$ref": "#/components/schemas/PaymentMethodType" + "gsm_rule_delete": { + "type": "boolean" }, - "card_networks": { - "$ref": "#/components/schemas/CardNetwork" + "connector": { + "type": "string", + "description": "The connector through which payment has gone through" }, "flow": { - "$ref": "#/components/schemas/FrmPreferredFlowTypes" + "type": "string", + "description": "The flow in which the code and message occurred for a connector" }, - "action": { - "$ref": "#/components/schemas/FrmAction" + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" } } }, - "FrmPreferredFlowTypes": { - "type": "string", - "enum": [ - "pre", - "post" - ] - }, - "FutureUsage": { - "type": "string", - "enum": [ - "off_session", - "on_session" - ] - }, - "GcashRedirection": { - "type": "object" - }, - "GiftCardData": { - "oneOf": [ - { - "type": "object", - "required": [ - "givex" - ], - "properties": { - "givex": { - "$ref": "#/components/schemas/GiftCardDetails" - } - } + "GsmResponse": { + "type": "object", + "required": [ + "connector", + "flow", + "sub_flow", + "code", + "message", + "status", + "decision", + "step_up_possible" + ], + "properties": { + "connector": { + "type": "string", + "description": "The connector through which payment has gone through" }, - { - "type": "object", - "required": [ - "pay_safe_card" - ], - "properties": { - "pay_safe_card": { - "type": "object" - } - } + "flow": { + "type": "string", + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + }, + "message": { + "type": "string", + "description": "message received from the connector" + }, + "status": { + "type": "string", + "description": "status provided by the router" + }, + "router_error": { + "type": "string", + "description": "optional error provided by the router", + "nullable": true + }, + "decision": { + "type": "string", + "description": "decision to be taken for auto retries flow" + }, + "step_up_possible": { + "type": "boolean", + "description": "indicates if step_up retry is possible" + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors", + "nullable": true } - ] + } }, - "GiftCardDetails": { + "GsmRetrieveRequest": { "type": "object", "required": [ - "number", - "cvc" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "number": { + "connector": { + "$ref": "#/components/schemas/Connector" + }, + "flow": { + "type": "string", + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { "type": "string", - "description": "The gift card number" + "description": "The sub_flow in which the code and message occurred for a connector" }, - "cvc": { + "code": { "type": "string", - "description": "The card verification code." + "description": "code received from the connector" + }, + "message": { + "type": "string", + "description": "message received from the connector" } } }, - "GoPayRedirection": { - "type": "object" - }, - "GooglePayPaymentMethodInfo": { + "GsmUpdateRequest": { "type": "object", "required": [ - "card_network", - "card_details" + "connector", + "flow", + "sub_flow", + "code", + "message" ], "properties": { - "card_network": { + "connector": { "type": "string", - "description": "The name of the card network" + "description": "The connector through which payment has gone through" }, - "card_details": { + "flow": { "type": "string", - "description": "The details of the card" + "description": "The flow in which the code and message occurred for a connector" + }, + "sub_flow": { + "type": "string", + "description": "The sub_flow in which the code and message occurred for a connector" + }, + "code": { + "type": "string", + "description": "code received from the connector" + }, + "message": { + "type": "string", + "description": "message received from the connector" + }, + "status": { + "type": "string", + "description": "status provided by the router", + "nullable": true + }, + "router_error": { + "type": "string", + "description": "optional error provided by the router", + "nullable": true + }, + "decision": { + "allOf": [ + { + "$ref": "#/components/schemas/GsmDecision" + } + ], + "nullable": true + }, + "step_up_possible": { + "type": "boolean", + "description": "indicates if step_up retry is possible", + "nullable": true + }, + "unified_code": { + "type": "string", + "description": "error code unified across the connectors", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors", + "nullable": true } } }, - "GooglePayRedirectData": { - "type": "object" - }, - "GooglePaySessionResponse": { + "IfStatement": { "type": "object", + "description": "Represents an IF statement with conditions and optional nested IF statements\n\n```text\npayment.method = card {\npayment.method.cardtype = (credit, debit) {\npayment.method.network = (amex, rupay, diners)\n}\n}\n```", "required": [ - "merchant_info", - "allowed_payment_methods", - "transaction_info", - "delayed_session_token", - "connector", - "sdk_next_action" + "condition" ], "properties": { - "merchant_info": { - "$ref": "#/components/schemas/GpayMerchantInfo" + "condition": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Comparison" + } }, - "allowed_payment_methods": { + "nested": { "type": "array", "items": { - "$ref": "#/components/schemas/GpayAllowedPaymentMethods" + "$ref": "#/components/schemas/IfStatement" }, - "description": "List of the allowed payment meythods" + "nullable": true + } + } + }, + "IncrementalAuthorizationResponse": { + "type": "object", + "required": [ + "authorization_id", + "amount", + "status", + "previously_authorized_amount" + ], + "properties": { + "authorization_id": { + "type": "string", + "description": "The unique identifier of authorization" }, - "transaction_info": { - "$ref": "#/components/schemas/GpayTransactionInfo" + "amount": { + "type": "integer", + "format": "int64", + "description": "Amount the authorization has been made for" }, - "delayed_session_token": { - "type": "boolean", - "description": "Identifier for the delayed session response" + "status": { + "$ref": "#/components/schemas/AuthorizationStatus" }, - "connector": { + "error_code": { "type": "string", - "description": "The name of the connector" - }, - "sdk_next_action": { - "$ref": "#/components/schemas/SdkNextAction" + "description": "Error code sent by the connector for authorization", + "nullable": true }, - "secrets": { - "allOf": [ - { - "$ref": "#/components/schemas/SecretInfoToInitiateSdk" - } - ], + "error_message": { + "type": "string", + "description": "Error message sent by the connector for authorization", "nullable": true + }, + "previously_authorized_amount": { + "type": "integer", + "format": "int64", + "description": "Previously authorized amount for the payment" } } }, - "GooglePayThirdPartySdk": { + "IndomaretVoucherData": { "type": "object", "required": [ - "delayed_session_token", - "connector", - "sdk_next_action" + "first_name", + "last_name", + "email" ], "properties": { - "delayed_session_token": { - "type": "boolean", - "description": "Identifier for the delayed session response" + "first_name": { + "type": "string", + "description": "The billing first name for Alfamart", + "example": "Jane" }, - "connector": { + "last_name": { "type": "string", - "description": "The name of the connector" + "description": "The billing second name for Alfamart", + "example": "Doe" }, - "sdk_next_action": { - "$ref": "#/components/schemas/SdkNextAction" + "email": { + "type": "string", + "description": "The Email ID for Alfamart", + "example": "example@me.com" } } }, - "GooglePayThirdPartySdkData": { - "type": "object" + "IntentStatus": { + "type": "string", + "enum": [ + "succeeded", + "failed", + "cancelled", + "processing", + "requires_customer_action", + "requires_merchant_action", + "requires_payment_method", + "requires_confirmation", + "requires_capture", + "partially_captured", + "partially_captured_and_capturable" + ] }, - "GooglePayWalletData": { + "JCSVoucherData": { "type": "object", "required": [ - "type", - "description", - "info", - "tokenization_data" + "first_name", + "last_name", + "email", + "phone_number" ], "properties": { - "type": { + "first_name": { "type": "string", - "description": "The type of payment method" + "description": "The billing first name for Japanese convenience stores", + "example": "Jane" }, - "description": { + "last_name": { "type": "string", - "description": "User-facing message to describe the payment method that funds this transaction." + "description": "The billing second name Japanese convenience stores", + "example": "Doe" }, - "info": { - "$ref": "#/components/schemas/GooglePayPaymentMethodInfo" + "email": { + "type": "string", + "description": "The Email ID for Japanese convenience stores", + "example": "example@me.com" }, - "tokenization_data": { - "$ref": "#/components/schemas/GpayTokenizationData" + "phone_number": { + "type": "string", + "description": "The telephone number for Japanese convenience stores", + "example": "9999999999" } } }, - "GpayAllowedMethodsParameters": { + "KakaoPayRedirection": { + "type": "object" + }, + "KlarnaSessionTokenResponse": { "type": "object", "required": [ - "allowed_auth_methods", - "allowed_card_networks" + "session_token", + "session_id" ], "properties": { - "allowed_auth_methods": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of allowed auth methods (ex: 3DS, No3DS, PAN_ONLY etc)" + "session_token": { + "type": "string", + "description": "The session token for Klarna" }, - "allowed_card_networks": { + "session_id": { + "type": "string", + "description": "The identifier for the session" + } + } + }, + "LinkedRoutingConfigRetrieveResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoutingRetrieveResponse" + }, + { "type": "array", "items": { - "type": "string" - }, - "description": "The list of allowed card networks (ex: AMEX,JCB etc)" + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } } - } + ] }, - "GpayAllowedPaymentMethods": { + "ListBlocklistQuery": { "type": "object", "required": [ - "type", - "parameters", - "tokenization_specification" + "data_kind" ], "properties": { - "type": { - "type": "string", - "description": "The type of payment method" + "data_kind": { + "$ref": "#/components/schemas/BlocklistDataKind" }, - "parameters": { - "$ref": "#/components/schemas/GpayAllowedMethodsParameters" + "limit": { + "type": "integer", + "format": "int32", + "minimum": 0 }, - "tokenization_specification": { - "$ref": "#/components/schemas/GpayTokenizationSpecification" + "offset": { + "type": "integer", + "format": "int32", + "minimum": 0 } } }, - "GpayMerchantInfo": { + "MandateAmountData": { "type": "object", "required": [ - "merchant_name" + "amount", + "currency" ], "properties": { - "merchant_id": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The maximum amount to be debited for the mandate transaction", + "example": 6540 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "start_date": { "type": "string", - "description": "The merchant Identifier that needs to be passed while invoking Gpay SDK", + "format": "date-time", + "description": "Specifying start date of the mandate", + "example": "2022-09-10T00:00:00Z", "nullable": true }, - "merchant_name": { + "end_date": { "type": "string", - "description": "The name of the merchant that needs to be displayed on Gpay PopUp" - } - } - }, - "GpaySessionTokenResponse": { - "oneOf": [ - { - "$ref": "#/components/schemas/GooglePayThirdPartySdk" + "format": "date-time", + "description": "Specifying end date of the mandate", + "example": "2023-09-10T23:59:59Z", + "nullable": true }, - { - "$ref": "#/components/schemas/GooglePaySessionResponse" + "metadata": { + "type": "object", + "description": "Additional details required by mandate", + "nullable": true } - ] + } }, - "GpayTokenParameters": { + "MandateCardDetails": { "type": "object", - "required": [ - "gateway" - ], "properties": { - "gateway": { + "last4_digits": { "type": "string", - "description": "The name of the connector" + "description": "The last 4 digits of card", + "nullable": true }, - "gateway_merchant_id": { + "card_exp_month": { "type": "string", - "description": "The merchant ID registered in the connector associated", + "description": "The expiry month of card", "nullable": true }, - "stripe:version": { + "card_exp_year": { "type": "string", + "description": "The expiry year of card", "nullable": true }, - "stripe:publishableKey": { + "card_holder_name": { "type": "string", + "description": "The card holder name", "nullable": true - } - } - }, - "GpayTokenizationData": { - "type": "object", - "required": [ - "type", - "token" - ], - "properties": { - "type": { + }, + "card_token": { "type": "string", - "description": "The type of the token" + "description": "The token from card locker", + "nullable": true }, - "token": { + "scheme": { "type": "string", - "description": "Token generated for the wallet" + "description": "The card scheme network for the particular card", + "nullable": true + }, + "issuer_country": { + "type": "string", + "description": "The country code in in which the card was issued", + "nullable": true + }, + "card_fingerprint": { + "type": "string", + "description": "A unique identifier alias to identify a particular card", + "nullable": true + }, + "card_isin": { + "type": "string", + "description": "The first 6 digits of card", + "nullable": true + }, + "card_issuer": { + "type": "string", + "description": "The bank that issued the card", + "nullable": true + }, + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" + } + ], + "nullable": true + }, + "card_type": { + "type": "string", + "description": "The type of the payment card", + "nullable": true } } }, - "GpayTokenizationSpecification": { + "MandateData": { "type": "object", - "required": [ - "type", - "parameters" - ], "properties": { - "type": { - "type": "string", - "description": "The token specification type(ex: PAYMENT_GATEWAY)" + "customer_acceptance": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerAcceptance" + } + ], + "nullable": true }, - "parameters": { - "$ref": "#/components/schemas/GpayTokenParameters" + "mandate_type": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateType" + } + ], + "nullable": true } } }, - "GpayTransactionInfo": { + "MandateResponse": { "type": "object", "required": [ - "country_code", - "currency_code", - "total_price_status", - "total_price" + "mandate_id", + "status", + "payment_method_id", + "payment_method" ], "properties": { - "country_code": { - "$ref": "#/components/schemas/CountryAlpha2" + "mandate_id": { + "type": "string", + "description": "The identifier for mandate" }, - "currency_code": { - "$ref": "#/components/schemas/Currency" + "status": { + "$ref": "#/components/schemas/MandateStatus" }, - "total_price_status": { + "payment_method_id": { "type": "string", - "description": "The total price status (ex: 'FINAL')" + "description": "The identifier for payment method" }, - "total_price": { + "payment_method": { "type": "string", - "description": "The total price" + "description": "The payment method" + }, + "payment_method_type": { + "type": "string", + "description": "The payment method type", + "nullable": true + }, + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateCardDetails" + } + ], + "nullable": true + }, + "customer_acceptance": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerAcceptance" + } + ], + "nullable": true } } }, - "GsmCreateRequest": { + "MandateRevokedResponse": { "type": "object", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message", - "status", - "decision", - "step_up_possible" + "mandate_id", + "status" ], "properties": { - "connector": { - "$ref": "#/components/schemas/Connector" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" + "mandate_id": { + "type": "string", + "description": "The identifier for mandate" }, "status": { - "type": "string" + "$ref": "#/components/schemas/MandateStatus" }, - "router_error": { + "error_code": { "type": "string", + "description": "If there was an error while calling the connectors the code is received here", + "example": "E0001", "nullable": true }, - "decision": { - "$ref": "#/components/schemas/GsmDecision" - }, - "step_up_possible": { - "type": "boolean" - }, - "unified_code": { + "error_message": { "type": "string", + "description": "If there was an error while calling the connector the error message is received here", + "example": "Failed while verifying the card", "nullable": true + } + } + }, + "MandateStatus": { + "type": "string", + "description": "The status of the mandate, which indicates whether it can be used to initiate a payment.", + "enum": [ + "active", + "inactive", + "pending", + "revoked" + ] + }, + "MandateType": { + "oneOf": [ + { + "type": "object", + "required": [ + "single_use" + ], + "properties": { + "single_use": { + "$ref": "#/components/schemas/MandateAmountData" + } + } }, - "unified_message": { - "type": "string", - "nullable": true + { + "type": "object", + "required": [ + "multi_use" + ], + "properties": { + "multi_use": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateAmountData" + } + ], + "nullable": true + } + } } - } - }, - "GsmDecision": { - "type": "string", - "enum": [ - "retry", - "requeue", - "do_default" ] }, - "GsmDeleteRequest": { + "MaskedBankDetails": { "type": "object", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message" + "mask" ], "properties": { - "connector": { - "type": "string" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { + "mask": { "type": "string" } } }, - "GsmDeleteResponse": { + "MbWayRedirection": { "type": "object", "required": [ - "gsm_rule_delete", - "connector", - "flow", - "sub_flow", - "code" + "telephone_number" ], "properties": { - "gsm_rule_delete": { - "type": "boolean" - }, - "connector": { - "type": "string" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" + "telephone_number": { + "type": "string", + "description": "Telephone number of the shopper. Should be Portuguese phone number." } } }, - "GsmResponse": { + "MerchantAccountCreate": { "type": "object", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message", - "status", - "decision", - "step_up_possible" + "merchant_id" ], "properties": { - "connector": { - "type": "string" + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "flow": { - "type": "string" + "merchant_name": { + "type": "string", + "description": "Name of the Merchant Account", + "example": "NewAge Retailer", + "nullable": true }, - "sub_flow": { - "type": "string" + "merchant_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantDetails" + } + ], + "nullable": true }, - "code": { - "type": "string" + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 }, - "message": { - "type": "string" + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true }, - "status": { - "type": "string" + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], + "nullable": true }, - "router_error": { + "sub_merchants_enabled": { + "type": "boolean", + "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "parent_merchant_id": { + "type": "string", + "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 + }, + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "payment_response_hash_key": { "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", "nullable": true }, - "decision": { - "type": "string" + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true, + "nullable": true }, - "step_up_possible": { - "type": "boolean" + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - "unified_code": { + "publishable_key": { "type": "string", + "description": "API key that will be used for server side API access", + "example": "AH3423bkjbkjdsfbkj", "nullable": true }, - "unified_message": { + "locker_id": { + "type": "string", + "description": "An identifier for the vault used to store payment method information.", + "example": "locker_abc123", + "nullable": true + }, + "primary_business_details": { + "allOf": [ + { + "$ref": "#/components/schemas/PrimaryBusinessDetails" + } + ], + "nullable": true + }, + "frm_routing_algorithm": { + "type": "object", + "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "nullable": true + }, + "organization_id": { "type": "string", + "description": "The id of the organization to which the merchant belongs to", "nullable": true } } }, - "GsmRetrieveRequest": { + "MerchantAccountDeleteResponse": { "type": "object", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message" + "merchant_id", + "deleted" ], "properties": { - "connector": { - "$ref": "#/components/schemas/Connector" - }, - "flow": { - "type": "string" - }, - "sub_flow": { - "type": "string" - }, - "code": { - "type": "string" + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "message": { - "type": "string" + "deleted": { + "type": "boolean", + "description": "If the connector is deleted or not", + "example": false } } }, - "GsmUpdateRequest": { + "MerchantAccountResponse": { "type": "object", "required": [ - "connector", - "flow", - "sub_flow", - "code", - "message" + "merchant_id", + "enable_payment_response_hash", + "redirect_to_merchant_with_http_post", + "primary_business_details", + "organization_id", + "is_recon_enabled", + "recon_status" ], - "properties": { - "connector": { - "type": "string" - }, - "flow": { - "type": "string" + "properties": { + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "sub_flow": { - "type": "string" + "merchant_name": { + "type": "string", + "description": "Name of the Merchant Account", + "example": "NewAge Retailer", + "nullable": true }, - "code": { - "type": "string" + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 }, - "message": { - "type": "string" + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": false, + "example": true }, - "status": { + "payment_response_hash_key": { "type": "string", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 + }, + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true + }, + "merchant_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantDetails" + } + ], "nullable": true }, - "router_error": { - "type": "string", + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], "nullable": true }, - "decision": { + "payout_routing_algorithm": { "allOf": [ { - "$ref": "#/components/schemas/GsmDecision" + "$ref": "#/components/schemas/RoutingAlgorithm" } ], "nullable": true }, - "step_up_possible": { + "sub_merchants_enabled": { "type": "boolean", + "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "default": false, + "example": false, "nullable": true }, - "unified_code": { + "parent_merchant_id": { "type": "string", - "nullable": true + "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 }, - "unified_message": { + "publishable_key": { "type": "string", + "description": "API key that will be used for server side API access", + "example": "AH3423bkjbkjdsfbkj", "nullable": true - } - } - }, - "IncrementalAuthorizationResponse": { - "type": "object", - "required": [ - "authorization_id", - "amount", - "status", - "previously_authorized_amount" - ], - "properties": { - "authorization_id": { - "type": "string", - "description": "The unique identifier of authorization" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "Amount the authorization has been made for" }, - "status": { - "$ref": "#/components/schemas/AuthorizationStatus" + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - "error_code": { + "locker_id": { "type": "string", - "description": "Error code sent by the connector for authorization", + "description": "An identifier for the vault used to store payment method information.", + "example": "locker_abc123", "nullable": true }, - "error_message": { - "type": "string", - "description": "Error message sent by the connector for authorization", + "primary_business_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PrimaryBusinessDetails" + }, + "description": "Details about the primary business unit of the merchant account" + }, + "frm_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], "nullable": true }, - "previously_authorized_amount": { + "intent_fulfillment_time": { "type": "integer", "format": "int64", - "description": "Previously authorized amount for the payment" + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "nullable": true + }, + "organization_id": { + "type": "string", + "description": "The organization id merchant is associated with" + }, + "is_recon_enabled": { + "type": "boolean", + "description": "A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false" + }, + "default_profile": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments", + "nullable": true, + "maxLength": 64 + }, + "recon_status": { + "$ref": "#/components/schemas/ReconStatus" } } }, - "IndomaretVoucherData": { + "MerchantAccountUpdate": { "type": "object", "required": [ - "first_name", - "last_name", - "email" + "merchant_id" ], "properties": { - "first_name": { + "merchant_id": { "type": "string", - "description": "The billing first name for Alfamart", - "example": "Jane" + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 }, - "last_name": { + "merchant_name": { "type": "string", - "description": "The billing second name for Alfamart", - "example": "Doe" + "description": "Name of the Merchant Account", + "example": "NewAge Retailer", + "nullable": true }, - "email": { + "merchant_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantDetails" + } + ], + "nullable": true + }, + "return_url": { "type": "string", - "description": "The Email ID for Alfamart", - "example": "example@me.com" - } - } - }, - "IntentStatus": { - "type": "string", - "enum": [ - "succeeded", - "failed", - "cancelled", - "processing", - "requires_customer_action", - "requires_merchant_action", - "requires_payment_method", - "requires_confirmation", - "requires_capture", - "partially_captured", - "partially_captured_and_capturable" - ] - }, - "JCSVoucherData": { - "type": "object", - "required": [ - "first_name", - "last_name", - "email", - "phone_number" - ], - "properties": { - "first_name": { + "description": "The URL to redirect after the completion of the operation", + "example": "https://www.example.com/success", + "nullable": true, + "maxLength": 255 + }, + "webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookDetails" + } + ], + "nullable": true + }, + "payout_routing_algorithm": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingAlgorithm" + } + ], + "nullable": true + }, + "sub_merchants_enabled": { + "type": "boolean", + "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "default": false, + "example": false, + "nullable": true + }, + "parent_merchant_id": { "type": "string", - "description": "The billing first name for Japanese convenience stores", - "example": "Jane" + "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", + "example": "xkkdf909012sdjki2dkh5sdf", + "nullable": true, + "maxLength": 255 }, - "last_name": { + "enable_payment_response_hash": { + "type": "boolean", + "description": "A boolean value to indicate if payment response hash needs to be enabled", + "default": false, + "example": true, + "nullable": true + }, + "payment_response_hash_key": { "type": "string", - "description": "The billing second name Japanese convenience stores", - "example": "Doe" + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used.", + "nullable": true + }, + "redirect_to_merchant_with_http_post": { + "type": "boolean", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "default": false, + "example": true, + "nullable": true }, - "email": { - "type": "string", - "description": "The Email ID for Japanese convenience stores", - "example": "example@me.com" + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - "phone_number": { - "type": "string", - "description": "The telephone number for Japanese convenience stores", - "example": "9999999999" - } - } - }, - "KakaoPayRedirection": { - "type": "object" - }, - "KlarnaSessionTokenResponse": { - "type": "object", - "required": [ - "session_token", - "session_id" - ], - "properties": { - "session_token": { + "publishable_key": { "type": "string", - "description": "The session token for Klarna" + "description": "API key that will be used for server side API access", + "example": "AH3423bkjbkjdsfbkj", + "nullable": true }, - "session_id": { + "locker_id": { "type": "string", - "description": "The identifier for the session" - } - } - }, - "ListBlocklistQuery": { - "type": "object", - "required": [ - "data_kind" - ], - "properties": { - "data_kind": { - "$ref": "#/components/schemas/BlocklistDataKind" + "description": "An identifier for the vault used to store payment method information.", + "example": "locker_abc123", + "nullable": true }, - "limit": { - "type": "integer", - "format": "int32", - "minimum": 0 + "primary_business_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PrimaryBusinessDetails" + }, + "description": "Details about the primary business unit of the merchant account", + "nullable": true }, - "offset": { - "type": "integer", - "format": "int32", - "minimum": 0 + "frm_routing_algorithm": { + "type": "object", + "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "nullable": true + }, + "default_profile": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments\nTo unset this field, pass an empty string", + "nullable": true, + "maxLength": 64 } } }, - "MandateAmountData": { + "MerchantConnectorCreate": { "type": "object", + "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "amount", - "currency" + "connector_type", + "connector_name" ], "properties": { - "amount": { - "type": "integer", - "format": "int64", - "description": "The maximum amount to be debited for the mandate transaction", - "example": 6540 + "connector_type": { + "$ref": "#/components/schemas/ConnectorType" }, - "currency": { - "$ref": "#/components/schemas/Currency" + "connector_name": { + "$ref": "#/components/schemas/Connector" }, - "start_date": { + "connector_label": { "type": "string", - "format": "date-time", - "description": "Specifying start date of the mandate", - "example": "2022-09-10T00:00:00Z", + "description": "This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default`", + "example": "stripe_US_travel", "nullable": true }, - "end_date": { + "profile_id": { "type": "string", - "format": "date-time", - "description": "Specifying end date of the mandate", - "example": "2023-09-10T23:59:59Z", + "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", + "nullable": true + }, + "connector_account_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetails" + } + ], + "nullable": true + }, + "payment_methods_enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodsEnabled" + }, + "description": "An object containing the details about the payment methods that need to be enabled under this merchant connector account", + "example": [ + { + "accepted_countries": { + "list": [ + "FR", + "DE", + "IN" + ], + "type": "disable_only" + }, + "accepted_currencies": { + "list": [ + "USD", + "EUR" + ], + "type": "enable_only" + }, + "installment_payment_enabled": true, + "maximum_amount": 68607706, + "minimum_amount": 1, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ], + "payment_method_types": [ + "upi_collect", + "upi_intent" + ], + "payment_schemes": [ + "Discover", + "Discover" + ], + "recurring_enabled": true + } + ], + "nullable": true + }, + "connector_webhook_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + } + ], "nullable": true }, "metadata": { "type": "object", - "description": "Additional details required by mandate", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true - } - } - }, - "MandateCardDetails": { - "type": "object", - "properties": { - "last4_digits": { - "type": "string", - "description": "The last 4 digits of card", + }, + "test_mode": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", + "default": false, + "example": false, "nullable": true }, - "card_exp_month": { - "type": "string", - "description": "The expiry month of card", + "disabled": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", + "default": false, + "example": false, "nullable": true }, - "card_exp_year": { - "type": "string", - "description": "The expiry year of card", + "frm_configs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmConfigs" + }, + "description": "Contains the frm configs for the merchant connector", + "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", "nullable": true }, - "card_holder_name": { - "type": "string", - "description": "The card holder name", + "business_country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" + } + ], "nullable": true }, - "card_token": { + "business_label": { "type": "string", - "description": "The token from card locker", + "description": "The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", "nullable": true }, - "scheme": { + "business_sub_label": { "type": "string", - "description": "The card scheme network for the particular card", + "description": "The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "example": "chase", "nullable": true }, - "issuer_country": { + "merchant_connector_id": { "type": "string", - "description": "The country code in in which the card was issued", + "description": "Unique ID of the connector", + "example": "mca_5apGeP94tMts6rg3U3kR", "nullable": true }, - "card_fingerprint": { - "type": "string", - "description": "A unique identifier alias to identify a particular card", + "pm_auth_config": { "nullable": true }, - "card_isin": { + "status": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorStatus" + } + ], + "nullable": true + } + } + }, + "MerchantConnectorDeleteResponse": { + "type": "object", + "required": [ + "merchant_id", + "merchant_connector_id", + "deleted" + ], + "properties": { + "merchant_id": { "type": "string", - "description": "The first 6 digits of card", + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 255 + }, + "merchant_connector_id": { + "type": "string", + "description": "Unique ID of the connector", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "deleted": { + "type": "boolean", + "description": "If the connector is deleted or not", + "example": false + } + } + }, + "MerchantConnectorDetails": { + "type": "object", + "properties": { + "connector_account_details": { + "type": "object", + "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", "nullable": true }, - "card_issuer": { - "type": "string", - "description": "The bank that issued the card", + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true + } + } + }, + "MerchantConnectorDetailsWrap": { + "type": "object", + "required": [ + "creds_identifier" + ], + "properties": { + "creds_identifier": { + "type": "string", + "description": "Creds Identifier is to uniquely identify the credentials. Do not send any sensitive info in this field. And do not send the string \"null\"." }, - "card_network": { + "encoded_data": { "allOf": [ { - "$ref": "#/components/schemas/CardNetwork" + "$ref": "#/components/schemas/MerchantConnectorDetails" } ], "nullable": true - }, - "card_type": { - "type": "string", - "description": "The type of the payment card", - "nullable": true } } }, - "MandateData": { + "MerchantConnectorId": { "type": "object", + "required": [ + "merchant_id", + "merchant_connector_id" + ], "properties": { - "customer_acceptance": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerAcceptance" - } - ], - "nullable": true + "merchant_id": { + "type": "string" }, - "mandate_type": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateType" - } - ], - "nullable": true + "merchant_connector_id": { + "type": "string" } } }, - "MandateResponse": { + "MerchantConnectorResponse": { "type": "object", + "description": "Response of creating a new Merchant Connector for the merchant account.\"", "required": [ - "mandate_id", - "status", - "payment_method_id", - "payment_method" + "connector_type", + "connector_name", + "merchant_connector_id", + "status" ], "properties": { - "mandate_id": { - "type": "string", - "description": "The identifier for mandate" + "connector_type": { + "$ref": "#/components/schemas/ConnectorType" }, - "status": { - "$ref": "#/components/schemas/MandateStatus" + "connector_name": { + "$ref": "#/components/schemas/Connector" }, - "payment_method_id": { + "connector_label": { "type": "string", - "description": "The identifier for payment method" + "description": "A unique label to identify the connector account created under a business profile", + "example": "stripe_US_travel", + "nullable": true }, - "payment_method": { + "merchant_connector_id": { "type": "string", - "description": "The payment method" + "description": "Unique ID of the merchant connector account", + "example": "mca_5apGeP94tMts6rg3U3kR" }, - "payment_method_type": { + "profile_id": { "type": "string", - "description": "The payment method type", - "nullable": true + "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", + "nullable": true, + "maxLength": 64 }, - "card": { + "connector_account_details": { "allOf": [ { - "$ref": "#/components/schemas/MandateCardDetails" + "$ref": "#/components/schemas/MerchantConnectorDetails" } ], "nullable": true }, - "customer_acceptance": { + "payment_methods_enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodsEnabled" + }, + "description": "An object containing the details about the payment methods that need to be enabled under this merchant connector account", + "example": [ + { + "accepted_countries": { + "list": [ + "FR", + "DE", + "IN" + ], + "type": "disable_only" + }, + "accepted_currencies": { + "list": [ + "USD", + "EUR" + ], + "type": "enable_only" + }, + "installment_payment_enabled": true, + "maximum_amount": 68607706, + "minimum_amount": 1, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ], + "payment_method_types": [ + "upi_collect", + "upi_intent" + ], + "payment_schemes": [ + "Discover", + "Discover" + ], + "recurring_enabled": true + } + ], + "nullable": true + }, + "connector_webhook_details": { "allOf": [ { - "$ref": "#/components/schemas/CustomerAcceptance" + "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" } ], "nullable": true - } - } - }, - "MandateRevokedResponse": { - "type": "object", - "required": [ - "mandate_id", - "status" - ], - "properties": { - "mandate_id": { - "type": "string", - "description": "The identifier for mandate" }, - "status": { - "$ref": "#/components/schemas/MandateStatus" + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true }, - "error_code": { - "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", + "test_mode": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", + "default": false, + "example": false, "nullable": true }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", + "disabled": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", + "default": false, + "example": false, "nullable": true - } - } - }, - "MandateStatus": { - "type": "string", - "description": "The status of the mandate, which indicates whether it can be used to initiate a payment", - "enum": [ - "active", - "inactive", - "pending", - "revoked" - ] - }, - "MandateType": { - "oneOf": [ - { - "type": "object", - "required": [ - "single_use" - ], - "properties": { - "single_use": { - "$ref": "#/components/schemas/MandateAmountData" - } - } }, - { - "type": "object", - "required": [ - "multi_use" - ], - "properties": { - "multi_use": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateAmountData" - } - ], - "nullable": true + "frm_configs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmConfigs" + }, + "description": "Contains the frm configs for the merchant connector", + "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "nullable": true + }, + "business_country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" } - } - } - ] - }, - "MaskedBankDetails": { - "type": "object", - "required": [ - "mask" - ], - "properties": { - "mask": { - "type": "string" - } - } - }, - "MbWayRedirection": { - "type": "object", - "required": [ - "telephone_number" - ], - "properties": { - "telephone_number": { + ], + "nullable": true + }, + "business_label": { "type": "string", - "description": "Telephone number of the shopper. Should be Portuguese phone number." + "description": "The business label to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "example": "travel", + "nullable": true + }, + "business_sub_label": { + "type": "string", + "description": "The business sublabel to which the connector account is attached. To be deprecated soon. Use the 'profile_id' instead", + "example": "chase", + "nullable": true + }, + "applepay_verified_domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "identifier for the verified domains of a particular connector account", + "nullable": true + }, + "pm_auth_config": { + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" } } }, - "MerchantAccountCreate": { + "MerchantConnectorUpdate": { "type": "object", + "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "merchant_id" + "connector_type", + "status" ], "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 + "connector_type": { + "$ref": "#/components/schemas/ConnectorType" }, - "merchant_name": { + "connector_label": { "type": "string", - "description": "Name of the Merchant Account", - "example": "NewAge Retailer", + "description": "This is an unique label you can generate and pass in order to identify this connector account on your Hyperswitch dashboard and reports. Eg: if your profile label is `default`, connector label can be `stripe_default`", + "example": "stripe_US_travel", "nullable": true }, - "merchant_details": { + "connector_account_details": { "allOf": [ { - "$ref": "#/components/schemas/MerchantDetails" + "$ref": "#/components/schemas/MerchantConnectorDetails" } ], "nullable": true }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://www.example.com/success", - "nullable": true, - "maxLength": 255 + "payment_methods_enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodsEnabled" + }, + "description": "An object containing the details about the payment methods that need to be enabled under this merchant connector account", + "example": [ + { + "accepted_countries": { + "list": [ + "FR", + "DE", + "IN" + ], + "type": "disable_only" + }, + "accepted_currencies": { + "list": [ + "USD", + "EUR" + ], + "type": "enable_only" + }, + "installment_payment_enabled": true, + "maximum_amount": 68607706, + "minimum_amount": 1, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" + ], + "payment_method_types": [ + "upi_collect", + "upi_intent" + ], + "payment_schemes": [ + "Discover", + "Discover" + ], + "recurring_enabled": true + } + ], + "nullable": true }, - "webhook_details": { + "connector_webhook_details": { "allOf": [ { - "$ref": "#/components/schemas/WebhookDetails" + "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" } ], "nullable": true }, - "routing_algorithm": { + "metadata": { "type": "object", - "description": "The routing algorithm to be used for routing payments to desired connectors", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true }, - "payout_routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" - } - ], + "test_mode": { + "type": "boolean", + "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", + "default": false, + "example": false, "nullable": true }, - "sub_merchants_enabled": { + "disabled": { "type": "boolean", - "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", + "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", "default": false, "example": false, "nullable": true }, - "parent_merchant_id": { + "frm_configs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FrmConfigs" + }, + "description": "Contains the frm configs for the merchant connector", + "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "nullable": true + }, + "pm_auth_config": { + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/ConnectorStatus" + } + } + }, + "MerchantConnectorWebhookDetails": { + "type": "object", + "required": [ + "merchant_secret", + "additional_secret" + ], + "properties": { + "merchant_secret": { "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", + "example": "12345678900987654321" + }, + "additional_secret": { + "type": "string", + "example": "12345678900987654321" + } + } + }, + "MerchantDetails": { + "type": "object", + "properties": { + "primary_contact_person": { + "type": "string", + "description": "The merchant's primary contact name", + "example": "John Doe", "nullable": true, "maxLength": 255 }, - "enable_payment_response_hash": { - "type": "boolean", - "description": "A boolean value to indicate if payment response hash needs to be enabled", - "default": false, - "example": true, - "nullable": true + "primary_phone": { + "type": "string", + "description": "The merchant's primary phone number", + "example": "999999999", + "nullable": true, + "maxLength": 255 }, - "payment_response_hash_key": { + "primary_email": { "type": "string", - "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response\nIf the value is not provided, a default value is used", - "nullable": true + "description": "The merchant's primary email address", + "example": "johndoe@test.com", + "nullable": true, + "maxLength": 255 }, - "redirect_to_merchant_with_http_post": { - "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", - "default": false, - "example": true, - "nullable": true + "secondary_contact_person": { + "type": "string", + "description": "The merchant's secondary contact name", + "example": "John Doe2", + "nullable": true, + "maxLength": 255 }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true + "secondary_phone": { + "type": "string", + "description": "The merchant's secondary phone number", + "example": "999999988", + "nullable": true, + "maxLength": 255 }, - "publishable_key": { + "secondary_email": { "type": "string", - "description": "API key that will be used for server side API access", - "example": "AH3423bkjbkjdsfbkj", - "nullable": true + "description": "The merchant's secondary email address", + "example": "johndoe2@test.com", + "nullable": true, + "maxLength": 255 }, - "locker_id": { + "website": { "type": "string", - "description": "An identifier for the vault used to store payment method information.", - "example": "locker_abc123", - "nullable": true + "description": "The business website of the merchant", + "example": "www.example.com", + "nullable": true, + "maxLength": 255 }, - "primary_business_details": { + "about_business": { + "type": "string", + "description": "A brief description about merchant's business", + "example": "Online Retail with a wide selection of organic products for North America", + "nullable": true, + "maxLength": 255 + }, + "address": { "allOf": [ { - "$ref": "#/components/schemas/PrimaryBusinessDetails" + "$ref": "#/components/schemas/AddressDetails" } ], "nullable": true + } + } + }, + "MerchantRoutingAlgorithm": { + "type": "object", + "description": "Routing Algorithm specific to merchants", + "required": [ + "id", + "name", + "description", + "algorithm", + "created_at", + "modified_at" + ], + "properties": { + "id": { + "type": "string" }, - "frm_routing_algorithm": { - "type": "object", - "description": "The frm routing algorithm to be used for routing payments to desired FRM's", - "nullable": true + "name": { + "type": "string" }, - "organization_id": { - "type": "string", - "description": "The id of the organization to which the merchant belongs to", - "nullable": true + "description": { + "type": "string" + }, + "algorithm": { + "$ref": "#/components/schemas/RoutingAlgorithm" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "modified_at": { + "type": "integer", + "format": "int64" + } + } + }, + "MetadataValue": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" } } }, - "MerchantAccountDeleteResponse": { + "MobilePayRedirection": { + "type": "object" + }, + "MomoRedirection": { + "type": "object" + }, + "MultibancoBillingDetails": { "type": "object", "required": [ - "merchant_id", - "deleted" + "email" ], "properties": { - "merchant_id": { + "email": { "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 - }, - "deleted": { - "type": "boolean", - "description": "If the connector is deleted or not", - "example": false + "example": "example@me.com" } } }, - "MerchantAccountResponse": { + "MultibancoTransferInstructions": { "type": "object", "required": [ - "merchant_id", - "enable_payment_response_hash", - "redirect_to_merchant_with_http_post", - "primary_business_details", - "organization_id", - "is_recon_enabled", - "recon_status" + "reference", + "entity" ], "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 - }, - "merchant_name": { - "type": "string", - "description": "Name of the Merchant Account", - "example": "NewAge Retailer", - "nullable": true - }, - "return_url": { + "reference": { "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://www.example.com/success", - "nullable": true, - "maxLength": 255 - }, - "enable_payment_response_hash": { - "type": "boolean", - "description": "A boolean value to indicate if payment response hash needs to be enabled", - "default": false, - "example": true + "example": "122385736258" }, - "payment_response_hash_key": { + "entity": { "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, - "maxLength": 255 - }, - "redirect_to_merchant_with_http_post": { - "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", - "default": false, - "example": true - }, - "merchant_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantDetails" + "example": "12345" + } + } + }, + "NextActionCall": { + "type": "string", + "enum": [ + "confirm", + "sync" + ] + }, + "NextActionData": { + "oneOf": [ + { + "type": "object", + "description": "Contains the url for redirection flow", + "required": [ + "redirect_to_url", + "type" + ], + "properties": { + "redirect_to_url": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "redirect_to_url" + ] } + } + }, + { + "type": "object", + "description": "Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc)", + "required": [ + "bank_transfer_steps_and_charges_details", + "type" ], - "nullable": true + "properties": { + "bank_transfer_steps_and_charges_details": { + "$ref": "#/components/schemas/BankTransferNextStepsData" + }, + "type": { + "type": "string", + "enum": [ + "display_bank_transfer_information" + ] + } + } }, - "webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookDetails" + { + "type": "object", + "description": "Contains third party sdk session token response", + "required": [ + "type" + ], + "properties": { + "session_token": { + "allOf": [ + { + "$ref": "#/components/schemas/SessionToken" + } + ], + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "third_party_sdk_session_token" + ] } + } + }, + { + "type": "object", + "description": "Contains url for Qr code image, this qr code has to be shown in sdk", + "required": [ + "image_data_url", + "qr_code_url", + "type" ], - "nullable": true + "properties": { + "image_data_url": { + "type": "string", + "description": "Hyperswitch generated image data source url" + }, + "display_to_timestamp": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "qr_code_url": { + "type": "string", + "description": "The url for Qr code given by the connector" + }, + "type": { + "type": "string", + "enum": [ + "qr_code_information" + ] + } + } }, - "routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" + { + "type": "object", + "description": "Contains the download url and the reference number for transaction", + "required": [ + "voucher_details", + "type" + ], + "properties": { + "voucher_details": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "display_voucher_information" + ] } + } + }, + { + "type": "object", + "description": "Contains duration for displaying a wait screen, wait screen with timer is displayed by sdk", + "required": [ + "display_from_timestamp", + "type" ], - "nullable": true + "properties": { + "display_from_timestamp": { + "type": "integer" + }, + "display_to_timestamp": { + "type": "integer", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "wait_screen_information" + ] + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "NextActionType": { + "type": "string", + "enum": [ + "redirect_to_url", + "display_qr_code", + "invoke_sdk_client", + "trigger_api", + "display_bank_transfer_information", + "display_wait_screen" + ] + }, + "NoThirdPartySdkSessionResponse": { + "type": "object", + "required": [ + "epoch_timestamp", + "expires_at", + "merchant_session_identifier", + "nonce", + "merchant_identifier", + "domain_name", + "display_name", + "signature", + "operational_analytics_identifier", + "retries", + "psp_id" + ], + "properties": { + "epoch_timestamp": { + "type": "integer", + "format": "int64", + "description": "Timestamp at which session is requested", + "minimum": 0 }, - "payout_routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" - } - ], - "nullable": true + "expires_at": { + "type": "integer", + "format": "int64", + "description": "Timestamp at which session expires", + "minimum": 0 }, - "sub_merchants_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", - "default": false, - "example": false, - "nullable": true + "merchant_session_identifier": { + "type": "string", + "description": "The identifier for the merchant session" }, - "parent_merchant_id": { + "nonce": { "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, - "maxLength": 255 + "description": "Apple pay generated unique ID (UUID) value" }, - "publishable_key": { + "merchant_identifier": { "type": "string", - "description": "API key that will be used for server side API access", - "example": "AH3423bkjbkjdsfbkj", - "nullable": true + "description": "The identifier for the merchant" }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true + "domain_name": { + "type": "string", + "description": "The domain name of the merchant which is registered in Apple Pay" }, - "locker_id": { + "display_name": { "type": "string", - "description": "An identifier for the vault used to store payment method information.", - "example": "locker_abc123", - "nullable": true + "description": "The name to be displayed on Apple Pay button" }, - "primary_business_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PrimaryBusinessDetails" - }, - "description": "Default business details for connector routing" + "signature": { + "type": "string", + "description": "A string which represents the properties of a payment" }, - "frm_routing_algorithm": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingAlgorithm" - } - ], - "nullable": true + "operational_analytics_identifier": { + "type": "string", + "description": "The identifier for the operational analytics" }, - "intent_fulfillment_time": { + "retries": { "type": "integer", - "format": "int64", - "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", - "nullable": true + "format": "int32", + "description": "The number of retries to get the session response", + "minimum": 0 }, - "organization_id": { + "psp_id": { "type": "string", - "description": "The organization id merchant is associated with" - }, - "is_recon_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false" + "description": "The identifier for the connector transaction" + } + } + }, + "NoonData": { + "type": "object", + "properties": { + "order_category": { + "type": "string", + "description": "Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like \"pay\", \"food\", or any other custom string set by the merchant in Noon's Dashboard)", + "nullable": true + } + } + }, + "NumberComparison": { + "type": "object", + "description": "Represents a number comparison for \"NumberComparisonArrayValue\"", + "required": [ + "comparisonType", + "number" + ], + "properties": { + "comparisonType": { + "$ref": "#/components/schemas/ComparisonType" }, - "default_profile": { + "number": { + "type": "integer", + "format": "int64" + } + } + }, + "OnlineMandate": { + "type": "object", + "required": [ + "ip_address", + "user_agent" + ], + "properties": { + "ip_address": { "type": "string", - "description": "The default business profile that must be used for creating merchant accounts and payments", - "nullable": true, - "maxLength": 64 + "description": "Ip address of the customer machine from which the mandate was created", + "example": "123.32.25.123" }, - "recon_status": { - "$ref": "#/components/schemas/ReconStatus" + "user_agent": { + "type": "string", + "description": "The user-agent of the customer's browser" } } }, - "MerchantAccountUpdate": { + "OrderDetails": { "type": "object", "required": [ - "merchant_id" + "product_name", + "quantity" ], "properties": { - "merchant_id": { + "product_name": { "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "description": "Name of the product that is being purchased", + "example": "shirt", "maxLength": 255 }, - "merchant_name": { - "type": "string", - "description": "Name of the Merchant Account", - "example": "NewAge Retailer", + "quantity": { + "type": "integer", + "format": "int32", + "description": "The quantity of the product to be purchased", + "example": 1, + "minimum": 0 + }, + "requires_shipping": { + "type": "boolean", "nullable": true }, - "merchant_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantDetails" - } - ], + "product_img_link": { + "type": "string", + "description": "The image URL of the product", "nullable": true }, - "return_url": { + "product_id": { "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://www.example.com/success", - "nullable": true, - "maxLength": 255 + "description": "ID of the product that is being purchased", + "nullable": true }, - "webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookDetails" - } - ], + "category": { + "type": "string", + "description": "Category of the product that is being purchased", "nullable": true }, - "routing_algorithm": { - "type": "object", - "description": "The routing algorithm to be used for routing payments to desired connectors", + "brand": { + "type": "string", + "description": "Brand of the product that is being purchased", "nullable": true }, - "payout_routing_algorithm": { + "product_type": { "allOf": [ { - "$ref": "#/components/schemas/RoutingAlgorithm" + "$ref": "#/components/schemas/ProductType" } ], "nullable": true - }, - "sub_merchants_enabled": { - "type": "boolean", - "description": "A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "parent_merchant_id": { + } + } + }, + "OrderDetailsWithAmount": { + "type": "object", + "required": [ + "product_name", + "quantity", + "amount" + ], + "properties": { + "product_name": { "type": "string", - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": "xkkdf909012sdjki2dkh5sdf", - "nullable": true, + "description": "Name of the product that is being purchased", + "example": "shirt", "maxLength": 255 }, - "enable_payment_response_hash": { - "type": "boolean", - "description": "A boolean value to indicate if payment response hash needs to be enabled", - "default": false, - "example": true, - "nullable": true + "quantity": { + "type": "integer", + "format": "int32", + "description": "The quantity of the product to be purchased", + "example": 1, + "minimum": 0 }, - "payment_response_hash_key": { - "type": "string", - "description": "Refers to the hash key used for payment response", - "nullable": true + "amount": { + "type": "integer", + "format": "int64", + "description": "the amount per quantity of product" }, - "redirect_to_merchant_with_http_post": { + "requires_shipping": { "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", - "default": false, - "example": true, - "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "publishable_key": { - "type": "string", - "description": "API key that will be used for server side API access", - "example": "AH3423bkjbkjdsfbkj", "nullable": true }, - "locker_id": { + "product_img_link": { "type": "string", - "description": "An identifier for the vault used to store payment method information.", - "example": "locker_abc123", + "description": "The image URL of the product", "nullable": true }, - "primary_business_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PrimaryBusinessDetails" - }, - "description": "Default business details for connector routing", + "product_id": { + "type": "string", + "description": "ID of the product that is being purchased", "nullable": true }, - "frm_routing_algorithm": { - "type": "object", - "description": "The frm routing algorithm to be used for routing payments to desired FRM's", + "category": { + "type": "string", + "description": "Category of the product that is being purchased", "nullable": true }, - "default_profile": { + "brand": { "type": "string", - "description": "The default business profile that must be used for creating merchant accounts and payments\nTo unset this field, pass an empty string", - "nullable": true, - "maxLength": 64 + "description": "Brand of the product that is being purchased", + "nullable": true + }, + "product_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ProductType" + } + ], + "nullable": true } } }, - "MerchantConnectorCreate": { + "OutgoingWebhook": { "type": "object", - "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", "required": [ - "connector_type", - "connector_name", - "status" + "merchant_id", + "event_id", + "event_type", + "content" ], "properties": { - "connector_type": { - "$ref": "#/components/schemas/ConnectorType" - }, - "connector_name": { - "$ref": "#/components/schemas/Connector" - }, - "connector_label": { + "merchant_id": { "type": "string", - "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", - "example": "stripe_US_travel", - "nullable": true + "description": "The merchant id of the merchant" }, - "merchant_connector_id": { + "event_id": { "type": "string", - "description": "Unique ID of the connector", - "example": "mca_5apGeP94tMts6rg3U3kR", - "nullable": true + "description": "The unique event id for each webhook" }, - "connector_account_details": { + "event_type": { + "$ref": "#/components/schemas/EventType" + }, + "content": { + "$ref": "#/components/schemas/OutgoingWebhookContent" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "The time at which webhook was sent" + } + } + }, + "OutgoingWebhookContent": { + "oneOf": [ + { "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", - "nullable": true + "title": "PaymentsResponse", + "required": [ + "type", + "object" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "payment_details" + ] + }, + "object": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } }, - "test_mode": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", - "default": false, - "example": false, - "nullable": true + { + "type": "object", + "title": "RefundResponse", + "required": [ + "type", + "object" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "refund_details" + ] + }, + "object": { + "$ref": "#/components/schemas/RefundResponse" + } + } }, - "disabled": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", - "default": false, - "example": false, - "nullable": true + { + "type": "object", + "title": "DisputeResponse", + "required": [ + "type", + "object" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "dispute_details" + ] + }, + "object": { + "$ref": "#/components/schemas/DisputeResponse" + } + } }, - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodsEnabled" - }, - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": [ - { - "payment_method": "wallet", - "payment_method_types": [ - "upi_collect", - "upi_intent" + { + "type": "object", + "title": "MandateResponse", + "required": [ + "type", + "object" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "mandate_details" + ] + }, + "object": { + "$ref": "#/components/schemas/MandateResponse" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "PayLaterData": { + "oneOf": [ + { + "type": "object", + "required": [ + "klarna_redirect" + ], + "properties": { + "klarna_redirect": { + "type": "object", + "description": "For KlarnaRedirect as PayLater Option", + "required": [ + "billing_email", + "billing_country" ], - "payment_method_issuers": [ - "labore magna ipsum", - "aute" + "properties": { + "billing_email": { + "type": "string", + "description": "The billing email" + }, + "billing_country": { + "$ref": "#/components/schemas/CountryAlpha2" + } + } + } + } + }, + { + "type": "object", + "required": [ + "klarna_sdk" + ], + "properties": { + "klarna_sdk": { + "type": "object", + "description": "For Klarna Sdk as PayLater Option", + "required": [ + "token" ], - "payment_schemes": [ - "Discover", - "Discover" + "properties": { + "token": { + "type": "string", + "description": "The token for the sdk workflow" + } + } + } + } + }, + { + "type": "object", + "required": [ + "affirm_redirect" + ], + "properties": { + "affirm_redirect": { + "type": "object", + "description": "For Affirm redirect as PayLater Option" + } + } + }, + { + "type": "object", + "required": [ + "afterpay_clearpay_redirect" + ], + "properties": { + "afterpay_clearpay_redirect": { + "type": "object", + "description": "For AfterpayClearpay redirect as PayLater Option", + "required": [ + "billing_email", + "billing_name" ], - "accepted_currencies": { - "type": "enable_only", - "list": [ - "USD", - "EUR" - ] - }, - "accepted_countries": { - "type": "disable_only", - "list": [ - "FR", - "DE", - "IN" - ] - }, - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true + "properties": { + "billing_email": { + "type": "string", + "description": "The billing email" + }, + "billing_name": { + "type": "string", + "description": "The billing name" + } + } } - ], - "nullable": true + } }, - "metadata": { + { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "frm_configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmConfigs" - }, - "description": "contains the frm configs for the merchant connector", - "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", - "nullable": true - }, - "business_country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" - } + "required": [ + "pay_bright_redirect" ], - "nullable": true - }, - "business_label": { - "type": "string", - "nullable": true - }, - "business_sub_label": { - "type": "string", - "description": "Business Sub label of the merchant", - "example": "chase", - "nullable": true - }, - "connector_webhook_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + "properties": { + "pay_bright_redirect": { + "type": "object", + "description": "For PayBright Redirect as PayLater Option" } - ], - "nullable": true - }, - "profile_id": { - "type": "string", - "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", - "nullable": true - }, - "pm_auth_config": { - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ConnectorStatus" - } - } - }, - "MerchantConnectorDeleteResponse": { - "type": "object", - "required": [ - "merchant_id", - "merchant_connector_id", - "deleted" - ], - "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 255 - }, - "merchant_connector_id": { - "type": "string", - "description": "Unique ID of the connector", - "example": "mca_5apGeP94tMts6rg3U3kR" + } }, - "deleted": { - "type": "boolean", - "description": "If the connector is deleted or not", - "example": false - } - } - }, - "MerchantConnectorDetails": { - "type": "object", - "properties": { - "connector_account_details": { + { "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", - "nullable": true + "required": [ + "walley_redirect" + ], + "properties": { + "walley_redirect": { + "type": "object", + "description": "For WalleyRedirect as PayLater Option" + } + } }, - "metadata": { + { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - } - } - }, - "MerchantConnectorDetailsWrap": { - "type": "object", - "required": [ - "creds_identifier" - ], - "properties": { - "creds_identifier": { - "type": "string", - "description": "Creds Identifier is to uniquely identify the credentials. Do not send any sensitive info in this field. And do not send the string \"null\"." - }, - "encoded_data": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetails" + "required": [ + "alma_redirect" + ], + "properties": { + "alma_redirect": { + "type": "object", + "description": "For Alma Redirection as PayLater Option" } + } + }, + { + "type": "object", + "required": [ + "atome_redirect" ], - "nullable": true + "properties": { + "atome_redirect": { + "type": "object" + } + } } - } + ] }, - "MerchantConnectorId": { + "PayPalWalletData": { "type": "object", "required": [ - "merchant_id", - "merchant_connector_id" + "token" ], "properties": { - "merchant_id": { - "type": "string" - }, - "merchant_connector_id": { - "type": "string" + "token": { + "type": "string", + "description": "Token generated for the Apple pay" } } }, - "MerchantConnectorResponse": { + "PaymentAttemptResponse": { "type": "object", - "description": "Response of creating a new Merchant Connector for the merchant account.\"", "required": [ - "connector_type", - "connector_name", - "merchant_connector_id", - "status" + "attempt_id", + "status", + "amount" ], "properties": { - "connector_type": { - "$ref": "#/components/schemas/ConnectorType" - }, - "connector_name": { - "type": "string", - "description": "Name of the Connector", - "example": "stripe" - }, - "connector_label": { - "type": "string", - "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", - "example": "stripe_US_travel", - "nullable": true - }, - "merchant_connector_id": { + "attempt_id": { "type": "string", - "description": "Unique ID of the connector", - "example": "mca_5apGeP94tMts6rg3U3kR" - }, - "connector_account_details": { - "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", - "nullable": true - }, - "test_mode": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "disabled": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", - "default": false, - "example": false, - "nullable": true + "description": "Unique identifier for the attempt" }, - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodsEnabled" - }, - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": [ - { - "payment_method": "wallet", - "payment_method_types": [ - "upi_collect", - "upi_intent" - ], - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ], - "payment_schemes": [ - "Discover", - "Discover" - ], - "accepted_currencies": { - "type": "enable_only", - "list": [ - "USD", - "EUR" - ] - }, - "accepted_countries": { - "type": "disable_only", - "list": [ - "FR", - "DE", - "IN" - ] - }, - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true - } - ], - "nullable": true + "status": { + "$ref": "#/components/schemas/AttemptStatus" }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment attempt amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.," }, - "business_country": { + "currency": { "allOf": [ { - "$ref": "#/components/schemas/CountryAlpha2" + "$ref": "#/components/schemas/Currency" } ], "nullable": true }, - "business_label": { + "connector": { "type": "string", - "description": "Business Type of the merchant", - "example": "travel", + "description": "The connector used for the payment", "nullable": true }, - "business_sub_label": { + "error_message": { "type": "string", - "description": "Business Sub label of the merchant", - "example": "chase", - "nullable": true - }, - "frm_configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmConfigs" - }, - "description": "contains the frm configs for the merchant connector", - "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", + "description": "If there was an error while calling the connector the error message is received here", "nullable": true }, - "connector_webhook_details": { + "payment_method": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + "$ref": "#/components/schemas/PaymentMethod" } ], "nullable": true }, - "profile_id": { - "type": "string", - "description": "The business profile this connector must be created in\ndefault value from merchant account is taken if not passed", - "nullable": true, - "maxLength": 64 - }, - "applepay_verified_domains": { - "type": "array", - "items": { - "type": "string" - }, - "description": "identifier for the verified domains of a particular connector account", - "nullable": true - }, - "pm_auth_config": { - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ConnectorStatus" - } - } - }, - "MerchantConnectorUpdate": { - "type": "object", - "description": "Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", - "required": [ - "connector_type", - "status" - ], - "properties": { - "connector_type": { - "$ref": "#/components/schemas/ConnectorType" - }, - "connector_label": { + "connector_transaction_id": { "type": "string", - "description": "Connector label for a connector, this can serve as a field to identify the connector as per business details", - "nullable": true - }, - "connector_account_details": { - "type": "object", - "description": "Account details of the Connector. You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Useful for storing additional, structured information on an object.", - "nullable": true - }, - "test_mode": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is in Test mode. By default, its value is false.", - "default": false, - "example": false, - "nullable": true - }, - "disabled": { - "type": "boolean", - "description": "A boolean value to indicate if the connector is disabled. By default, its value is false.", - "default": false, - "example": false, + "description": "A unique identifier for a payment provided by the connector", "nullable": true }, - "payment_methods_enabled": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodsEnabled" - }, - "description": "Refers to the Parent Merchant ID if the merchant being created is a sub-merchant", - "example": [ + "capture_method": { + "allOf": [ { - "payment_method": "wallet", - "payment_method_types": [ - "upi_collect", - "upi_intent" - ], - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ], - "payment_schemes": [ - "Discover", - "Discover" - ], - "accepted_currencies": { - "type": "enable_only", - "list": [ - "USD", - "EUR" - ] - }, - "accepted_countries": { - "type": "disable_only", - "list": [ - "FR", - "DE", - "IN" - ] - }, - "minimum_amount": 1, - "maximum_amount": 68607706, - "recurring_enabled": true, - "installment_payment_enabled": true + "$ref": "#/components/schemas/CaptureMethod" } ], "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "frm_configs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FrmConfigs" - }, - "description": "contains the frm configs for the merchant connector", - "example": "\n[{\"gateway\":\"stripe\",\"payment_methods\":[{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\",\"action\":\"cancel_txn\"},{\"payment_method_type\":\"debit\",\"card_networks\":[\"Visa\"],\"flow\":\"pre\"}]}]}]\n", - "nullable": true - }, - "connector_webhook_details": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorWebhookDetails" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "pm_auth_config": { - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/ConnectorStatus" - } - } - }, - "MerchantConnectorWebhookDetails": { - "type": "object", - "required": [ - "merchant_secret", - "additional_secret" - ], - "properties": { - "merchant_secret": { - "type": "string", - "example": "12345678900987654321" - }, - "additional_secret": { - "type": "string", - "example": "12345678900987654321" - } - } - }, - "MerchantDetails": { - "type": "object", - "properties": { - "primary_contact_person": { - "type": "string", - "description": "The merchant's primary contact name", - "example": "John Doe", - "nullable": true, - "maxLength": 255 - }, - "primary_phone": { - "type": "string", - "description": "The merchant's primary phone number", - "example": "999999999", - "nullable": true, - "maxLength": 255 - }, - "primary_email": { + "cancellation_reason": { "type": "string", - "description": "The merchant's primary email address", - "example": "johndoe@test.com", - "nullable": true, - "maxLength": 255 + "description": "If the payment was cancelled the reason provided here", + "nullable": true }, - "secondary_contact_person": { + "mandate_id": { "type": "string", - "description": "The merchant's secondary contact name", - "example": "John Doe2", - "nullable": true, - "maxLength": 255 + "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", + "nullable": true }, - "secondary_phone": { + "error_code": { "type": "string", - "description": "The merchant's secondary phone number", - "example": "999999988", - "nullable": true, - "maxLength": 255 + "description": "If there was an error while calling the connectors the code is received here", + "nullable": true }, - "secondary_email": { + "payment_token": { "type": "string", - "description": "The merchant's secondary email address", - "example": "johndoe2@test.com", - "nullable": true, - "maxLength": 255 + "description": "Provide a reference to a stored payment method", + "nullable": true }, - "website": { - "type": "string", - "description": "The business website of the merchant", - "example": "www.example.com", - "nullable": true, - "maxLength": 255 + "connector_metadata": { + "description": "additional data related to some connectors", + "nullable": true }, - "about_business": { - "type": "string", - "description": "A brief description about merchant's business", - "example": "Online Retail with a wide selection of organic products for North America", - "nullable": true, - "maxLength": 255 + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" + } + ], + "nullable": true }, - "address": { + "payment_method_type": { "allOf": [ { - "$ref": "#/components/schemas/AddressDetails" + "$ref": "#/components/schemas/PaymentMethodType" } ], "nullable": true - } - } - }, - "MobilePayRedirection": { - "type": "object" - }, - "MomoRedirection": { - "type": "object" - }, - "MultibancoBillingDetails": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { + }, + "reference_id": { + "type": "string", + "description": "reference to the payment at connector side", + "example": "993672945374576J", + "nullable": true + }, + "unified_code": { "type": "string", - "example": "example@me.com" + "description": "error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true } } }, - "MultibancoTransferInstructions": { - "type": "object", - "required": [ - "reference", - "entity" - ], - "properties": { - "reference": { - "type": "string", - "example": "122385736258" + "PaymentCreatePaymentLinkConfig": { + "allOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + } + ], + "nullable": true }, - "entity": { - "type": "string", - "example": "12345" + { + "type": "object" } - } + ] }, - "NextActionCall": { + "PaymentExperience": { "type": "string", + "description": "To indicate the type of payment experience that the customer would go through", "enum": [ - "confirm", - "sync" + "redirect_to_url", + "invoke_sdk_client", + "display_qr_code", + "one_click", + "link_wallet", + "invoke_payment_app", + "display_wait_screen" ] }, - "NextActionData": { + "PaymentIdType": { "oneOf": [ { "type": "object", - "description": "Contains the url for redirection flow", "required": [ - "redirect_to_url", - "type" + "PaymentIntentId" ], "properties": { - "redirect_to_url": { - "type": "string" - }, - "type": { + "PaymentIntentId": { "type": "string", - "enum": [ - "redirect_to_url" - ] + "description": "The identifier for payment intent" } } }, { "type": "object", - "description": "Informs the next steps for bank transfer and also contains the charges details (ex: amount received, amount charged etc)", "required": [ - "bank_transfer_steps_and_charges_details", - "type" + "ConnectorTransactionId" ], "properties": { - "bank_transfer_steps_and_charges_details": { - "$ref": "#/components/schemas/BankTransferNextStepsData" - }, - "type": { + "ConnectorTransactionId": { "type": "string", - "enum": [ - "display_bank_transfer_information" - ] + "description": "The identifier for connector transaction" } } }, { "type": "object", - "description": "Contains third party sdk session token response", "required": [ - "type" + "PaymentAttemptId" ], "properties": { - "session_token": { - "allOf": [ - { - "$ref": "#/components/schemas/SessionToken" - } - ], - "nullable": true - }, - "type": { + "PaymentAttemptId": { "type": "string", - "enum": [ - "third_party_sdk_session_token" - ] + "description": "The identifier for payment attempt" } } }, { "type": "object", - "description": "Contains url for Qr code image, this qr code has to be shown in sdk", "required": [ - "image_data_url", - "qr_code_url", - "type" + "PreprocessingId" ], "properties": { - "image_data_url": { - "type": "string", - "description": "Hyperswitch generated image data source url" - }, - "display_to_timestamp": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "qr_code_url": { - "type": "string", - "description": "The url for Qr code given by the connector" - }, - "type": { + "PreprocessingId": { "type": "string", - "enum": [ - "qr_code_information" - ] + "description": "The identifier for preprocessing step" } } + } + ] + }, + "PaymentLinkConfig": { + "type": "object", + "required": [ + "theme", + "logo", + "seller_name", + "sdk_layout" + ], + "properties": { + "theme": { + "type": "string", + "description": "custom theme for the payment link" }, - { - "type": "object", - "description": "Contains the download url and the reference number for transaction", - "required": [ - "voucher_details", - "type" - ], - "properties": { - "voucher_details": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "display_voucher_information" - ] - } - } + "logo": { + "type": "string", + "description": "merchant display logo" }, - { - "type": "object", - "description": "Contains duration for displaying a wait screen, wait screen with timer is displayed by sdk", - "required": [ - "display_from_timestamp", - "type" - ], - "properties": { - "display_from_timestamp": { - "type": "integer" - }, - "display_to_timestamp": { - "type": "integer", - "nullable": true - }, - "type": { - "type": "string", - "enum": [ - "wait_screen_information" - ] - } - } + "seller_name": { + "type": "string", + "description": "Custom merchant name for payment link" + }, + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk" } - ], - "discriminator": { - "propertyName": "type" } }, - "NextActionType": { - "type": "string", - "enum": [ - "redirect_to_url", - "display_qr_code", - "invoke_sdk_client", - "trigger_api", - "display_bank_transfer_information", - "display_wait_screen" - ] + "PaymentLinkConfigRequest": { + "type": "object", + "properties": { + "theme": { + "type": "string", + "description": "custom theme for the payment link", + "example": "#4E6ADD", + "nullable": true, + "maxLength": 255 + }, + "logo": { + "type": "string", + "description": "merchant display logo", + "example": "https://i.pinimg.com/736x/4d/83/5c/4d835ca8aafbbb15f84d07d926fda473.jpg", + "nullable": true, + "maxLength": 255 + }, + "seller_name": { + "type": "string", + "description": "Custom merchant name for payment link", + "example": "hyperswitch", + "nullable": true, + "maxLength": 255 + }, + "sdk_layout": { + "type": "string", + "description": "Custom layout for sdk", + "example": "accordion", + "nullable": true, + "maxLength": 255 + } + } }, - "NoThirdPartySdkSessionResponse": { + "PaymentLinkInitiateRequest": { "type": "object", "required": [ - "epoch_timestamp", - "expires_at", - "merchant_session_identifier", - "nonce", - "merchant_identifier", - "domain_name", - "display_name", - "signature", - "operational_analytics_identifier", - "retries", - "psp_id" + "merchant_id", + "payment_id" ], "properties": { - "epoch_timestamp": { - "type": "integer", - "format": "int64", - "description": "Timestamp at which session is requested", - "minimum": 0 + "merchant_id": { + "type": "string" }, - "expires_at": { - "type": "integer", - "format": "int64", - "description": "Timestamp at which session expires", - "minimum": 0 + "payment_id": { + "type": "string" + } + } + }, + "PaymentLinkResponse": { + "type": "object", + "required": [ + "link", + "payment_link_id" + ], + "properties": { + "link": { + "type": "string" }, - "merchant_session_identifier": { + "payment_link_id": { + "type": "string" + } + } + }, + "PaymentLinkStatus": { + "type": "string", + "enum": [ + "active", + "expired" + ] + }, + "PaymentListConstraints": { + "type": "object", + "properties": { + "customer_id": { "type": "string", - "description": "The identifier for the merchant session" + "description": "The identifier for customer", + "example": "cus_meowuwunwiuwiwqw", + "nullable": true }, - "nonce": { + "starting_after": { "type": "string", - "description": "Apple pay generated unique ID (UUID) value" + "description": "A cursor for use in pagination, fetch the next list after some object", + "example": "pay_fafa124123", + "nullable": true }, - "merchant_identifier": { + "ending_before": { "type": "string", - "description": "The identifier for the merchant" + "description": "A cursor for use in pagination, fetch the previous list before some object", + "example": "pay_fafa124123", + "nullable": true }, - "domain_name": { - "type": "string", - "description": "The domain name of the merchant which is registered in Apple Pay" + "limit": { + "type": "integer", + "format": "int32", + "description": "limit on the number of objects to return", + "default": 10, + "maximum": 100, + "minimum": 0 }, - "display_name": { + "created": { "type": "string", - "description": "The name to be displayed on Apple Pay button" + "format": "date-time", + "description": "The time at which payment is created", + "example": "2022-09-10T10:11:12Z", + "nullable": true }, - "signature": { + "created.lt": { "type": "string", - "description": "A string which represents the properties of a payment" + "format": "date-time", + "description": "Time less than the payment created time", + "example": "2022-09-10T10:11:12Z", + "nullable": true }, - "operational_analytics_identifier": { + "created.gt": { "type": "string", - "description": "The identifier for the operational analytics" - }, - "retries": { - "type": "integer", - "format": "int32", - "description": "The number of retries to get the session response", - "minimum": 0 + "format": "date-time", + "description": "Time greater than the payment created time", + "example": "2022-09-10T10:11:12Z", + "nullable": true }, - "psp_id": { + "created.lte": { "type": "string", - "description": "The identifier for the connector transaction" - } - } - }, - "NoonData": { - "type": "object", - "properties": { - "order_category": { + "format": "date-time", + "description": "Time less than or equals to the payment created time", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "created.gte": { "type": "string", - "description": "Information about the order category that merchant wants to specify at connector level. (e.g. In Noon Payments it can take values like \"pay\", \"food\", or any other custom string set by the merchant in Noon's Dashboard)", + "format": "date-time", + "description": "Time greater than or equals to the payment created time", + "example": "2022-09-10T10:11:12Z", "nullable": true } } }, - "OnlineMandate": { + "PaymentListResponse": { "type": "object", "required": [ - "ip_address", - "user_agent" + "size", + "data" ], "properties": { - "ip_address": { - "type": "string", - "description": "Ip address of the customer machine from which the mandate was created", - "example": "123.32.25.123" + "size": { + "type": "integer", + "description": "The number of payments included in the list", + "minimum": 0 }, - "user_agent": { - "type": "string", - "description": "The user-agent of the customer's browser" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentsResponse" + } } } }, - "OrderDetails": { + "PaymentMethod": { + "type": "string", + "description": "Indicates the type of payment method. Eg: 'card', 'wallet', etc.", + "enum": [ + "card", + "card_redirect", + "pay_later", + "wallet", + "bank_redirect", + "bank_transfer", + "crypto", + "bank_debit", + "reward", + "upi", + "voucher", + "gift_card" + ] + }, + "PaymentMethodCreate": { "type": "object", "required": [ - "product_name", - "quantity" + "payment_method" ], "properties": { - "product_name": { - "type": "string", - "description": "Name of the product that is being purchased", - "example": "shirt", - "maxLength": 255 - }, - "quantity": { - "type": "integer", - "format": "int32", - "description": "The quantity of the product to be purchased", - "example": 1, - "minimum": 0 + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" }, - "requires_shipping": { - "type": "boolean", + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], "nullable": true }, - "product_img_link": { + "payment_method_issuer": { "type": "string", - "description": "The image URL of the product", + "description": "The name of the bank/ provider issuing the payment method to the end user", + "example": "Citibank", "nullable": true }, - "product_id": { - "type": "string", - "description": "ID of the product that is being purchased", + "payment_method_issuer_code": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodIssuerCode" + } + ], "nullable": true }, - "category": { + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetail" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "customer_id": { "type": "string", - "description": "Category of the product that is being purchased", + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw", "nullable": true }, - "brand": { + "card_network": { "type": "string", - "description": "Brand of the product that is being purchased", + "description": "The card network", + "example": "Visa", "nullable": true }, - "product_type": { + "bank_transfer": { "allOf": [ { - "$ref": "#/components/schemas/ProductType" + "$ref": "#/components/schemas/Bank" } ], "nullable": true } } }, - "OrderDetailsWithAmount": { - "type": "object", - "required": [ - "product_name", - "quantity", - "amount" - ], - "properties": { - "product_name": { - "type": "string", - "description": "Name of the product that is being purchased", - "example": "shirt", - "maxLength": 255 - }, - "quantity": { - "type": "integer", - "format": "int32", - "description": "The quantity of the product to be purchased", - "example": 1, - "minimum": 0 - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "the amount per quantity of product" + "PaymentMethodData": { + "oneOf": [ + { + "type": "object", + "title": "Card", + "required": [ + "card" + ], + "properties": { + "card": { + "$ref": "#/components/schemas/Card" + } + } }, - "requires_shipping": { - "type": "boolean", - "nullable": true + { + "type": "object", + "title": "CardRedirect", + "required": [ + "card_redirect" + ], + "properties": { + "card_redirect": { + "$ref": "#/components/schemas/CardRedirectData" + } + } }, - "product_img_link": { - "type": "string", - "description": "The image URL of the product", - "nullable": true + { + "type": "object", + "title": "Wallet", + "required": [ + "wallet" + ], + "properties": { + "wallet": { + "$ref": "#/components/schemas/WalletData" + } + } }, - "product_id": { - "type": "string", - "description": "ID of the product that is being purchased", - "nullable": true + { + "type": "object", + "title": "PayLater", + "required": [ + "pay_later" + ], + "properties": { + "pay_later": { + "$ref": "#/components/schemas/PayLaterData" + } + } }, - "category": { - "type": "string", - "description": "Category of the product that is being purchased", - "nullable": true + { + "type": "object", + "title": "BankRedirect", + "required": [ + "bank_redirect" + ], + "properties": { + "bank_redirect": { + "$ref": "#/components/schemas/BankRedirectData" + } + } }, - "brand": { - "type": "string", - "description": "Brand of the product that is being purchased", - "nullable": true + { + "type": "object", + "title": "BankDebit", + "required": [ + "bank_debit" + ], + "properties": { + "bank_debit": { + "$ref": "#/components/schemas/BankDebitData" + } + } }, - "product_type": { - "allOf": [ - { - "$ref": "#/components/schemas/ProductType" + { + "type": "object", + "title": "BankTransfer", + "required": [ + "bank_transfer" + ], + "properties": { + "bank_transfer": { + "$ref": "#/components/schemas/BankTransferData" } + } + }, + { + "type": "object", + "title": "Crypto", + "required": [ + "crypto" ], - "nullable": true - } - } - }, - "OutgoingWebhook": { - "type": "object", - "required": [ - "merchant_id", - "event_id", - "event_type", - "content" - ], - "properties": { - "merchant_id": { - "type": "string", - "description": "The merchant id of the merchant" + "properties": { + "crypto": { + "$ref": "#/components/schemas/CryptoData" + } + } }, - "event_id": { + { "type": "string", - "description": "The unique event id for each webhook" - }, - "event_type": { - "$ref": "#/components/schemas/EventType" - }, - "content": { - "$ref": "#/components/schemas/OutgoingWebhookContent" + "title": "MandatePayment", + "enum": [ + "mandate_payment" + ] }, - "timestamp": { + { "type": "string", - "format": "date-time", - "description": "The time at which webhook was sent" - } - } - }, - "OutgoingWebhookContent": { - "oneOf": [ + "title": "Reward", + "enum": [ + "reward" + ] + }, { "type": "object", + "title": "Upi", "required": [ - "type", - "object" + "upi" ], "properties": { - "type": { - "type": "string", - "enum": [ - "payment_details" - ] - }, - "object": { - "$ref": "#/components/schemas/PaymentsResponse" + "upi": { + "$ref": "#/components/schemas/UpiData" } } }, { "type": "object", + "title": "Voucher", "required": [ - "type", - "object" + "voucher" ], "properties": { - "type": { - "type": "string", - "enum": [ - "refund_details" - ] - }, - "object": { - "$ref": "#/components/schemas/RefundResponse" + "voucher": { + "$ref": "#/components/schemas/VoucherData" } } }, { "type": "object", + "title": "GiftCard", "required": [ - "type", - "object" + "gift_card" ], "properties": { - "type": { - "type": "string", - "enum": [ - "dispute_details" - ] - }, - "object": { - "$ref": "#/components/schemas/DisputeResponse" + "gift_card": { + "$ref": "#/components/schemas/GiftCardData" } } }, { "type": "object", + "title": "CardToken", "required": [ - "type", - "object" + "card_token" ], "properties": { - "type": { - "type": "string", - "enum": [ - "mandate_details" + "card_token": { + "$ref": "#/components/schemas/CardToken" + } + } + } + ] + }, + "PaymentMethodDeleteResponse": { + "type": "object", + "required": [ + "payment_method_id", + "deleted" + ], + "properties": { + "payment_method_id": { + "type": "string", + "description": "The unique identifier of the Payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIy" + }, + "deleted": { + "type": "boolean", + "description": "Whether payment method was deleted or not", + "example": true + } + } + }, + "PaymentMethodIssuerCode": { + "type": "string", + "enum": [ + "jp_hdfc", + "jp_icici", + "jp_googlepay", + "jp_applepay", + "jp_phonepay", + "jp_wechat", + "jp_sofort", + "jp_giropay", + "jp_sepa", + "jp_bacs" + ] + }, + "PaymentMethodList": { + "type": "object", + "required": [ + "payment_method" + ], + "properties": { + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "This is a sub-category of payment method.", + "example": [ + "credit" + ], + "nullable": true + } + } + }, + "PaymentMethodListResponse": { + "type": "object", + "required": [ + "payment_methods", + "mandate_payment", + "show_surcharge_breakup_screen" + ], + "properties": { + "redirect_url": { + "type": "string", + "description": "Redirect URL of the merchant", + "example": "https://www.google.com", + "nullable": true + }, + "payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodList" + }, + "description": "Information about the payment method", + "example": [ + { + "payment_experience": null, + "payment_method": "wallet", + "payment_method_issuers": [ + "labore magna ipsum", + "aute" ] - }, - "object": { - "$ref": "#/components/schemas/MandateResponse" } - } + ] + }, + "mandate_payment": { + "$ref": "#/components/schemas/MandateType" + }, + "merchant_name": { + "type": "string", + "nullable": true + }, + "show_surcharge_breakup_screen": { + "type": "boolean", + "description": "flag to indicate if surcharge and tax breakup screen should be shown or not" + }, + "payment_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentType" + } + ], + "nullable": true } - ], - "discriminator": { - "propertyName": "type" } }, - "PayLaterData": { - "oneOf": [ - { - "type": "object", - "required": [ - "klarna_redirect" - ], - "properties": { - "klarna_redirect": { - "type": "object", - "description": "For KlarnaRedirect as PayLater Option", - "required": [ - "billing_email", - "billing_country" - ], - "properties": { - "billing_email": { - "type": "string", - "description": "The billing email" - }, - "billing_country": { - "$ref": "#/components/schemas/CountryAlpha2" - } - } - } - } + "PaymentMethodResponse": { + "type": "object", + "required": [ + "merchant_id", + "payment_method_id", + "payment_method", + "recurring_enabled", + "installment_payment_enabled" + ], + "properties": { + "merchant_id": { + "type": "string", + "description": "Unique identifier for a merchant", + "example": "merchant_1671528864" }, - { - "type": "object", - "required": [ - "klarna_sdk" + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw", + "nullable": true + }, + "payment_method_id": { + "type": "string", + "description": "The unique identifier of the Payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIy" + }, + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } ], - "properties": { - "klarna_sdk": { - "type": "object", - "description": "For Klarna Sdk as PayLater Option", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string", - "description": "The token for the sdk workflow" - } - } + "nullable": true + }, + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetailFromLocker" } - } + ], + "nullable": true }, - { - "type": "object", - "required": [ - "affirm_redirect" + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for recurring payments", + "example": true + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method is eligible for installment payments", + "example": true + }, + "payment_experience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentExperience" + }, + "description": "Type of payment experience enabled with the connector", + "example": [ + "redirect_to_url" ], - "properties": { - "affirm_redirect": { - "type": "object", - "description": "For Affirm redirect as PayLater Option" - } - } + "nullable": true }, - { + "metadata": { "type": "object", - "required": [ - "afterpay_clearpay_redirect" + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "created": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the customer was created", + "example": "2023-01-18T11:04:09.922Z", + "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" + } ], - "properties": { - "afterpay_clearpay_redirect": { - "type": "object", - "description": "For AfterpayClearpay redirect as PayLater Option", - "required": [ - "billing_email", - "billing_name" - ], - "properties": { - "billing_email": { - "type": "string", - "description": "The billing email" - }, - "billing_name": { - "type": "string", - "description": "The billing name" - } - } + "nullable": true + } + } + }, + "PaymentMethodType": { + "type": "string", + "description": "Indicates the sub type of payment method. Eg: 'google_pay' & 'apple_pay' for wallets.", + "enum": [ + "ach", + "affirm", + "afterpay_clearpay", + "alfamart", + "ali_pay", + "ali_pay_hk", + "alma", + "apple_pay", + "atome", + "bacs", + "bancontact_card", + "becs", + "benefit", + "bizum", + "blik", + "boleto", + "bca_bank_transfer", + "bni_va", + "bri_va", + "card_redirect", + "cimb_va", + "classic", + "credit", + "crypto_currency", + "cashapp", + "dana", + "danamon_va", + "debit", + "efecty", + "eps", + "evoucher", + "giropay", + "givex", + "google_pay", + "go_pay", + "gcash", + "ideal", + "interac", + "indomaret", + "klarna", + "kakao_pay", + "mandiri_va", + "knet", + "mb_way", + "mobile_pay", + "momo", + "momo_atm", + "multibanco", + "online_banking_thailand", + "online_banking_czech_republic", + "online_banking_finland", + "online_banking_fpx", + "online_banking_poland", + "online_banking_slovakia", + "oxxo", + "pago_efectivo", + "permata_bank_transfer", + "open_banking_uk", + "pay_bright", + "paypal", + "pix", + "pay_safe_card", + "przelewy24", + "pse", + "red_compra", + "red_pagos", + "samsung_pay", + "sepa", + "sofort", + "swish", + "touch_n_go", + "trustly", + "twint", + "upi_collect", + "vipps", + "walley", + "we_chat_pay", + "seven_eleven", + "lawson", + "mini_stop", + "family_mart", + "seicomart", + "pay_easy" + ] + }, + "PaymentMethodUpdate": { + "type": "object", + "properties": { + "card": { + "allOf": [ + { + "$ref": "#/components/schemas/CardDetail" } - } - }, - { - "type": "object", - "required": [ - "pay_bright_redirect" ], - "properties": { - "pay_bright_redirect": { - "type": "object", - "description": "For PayBright Redirect as PayLater Option" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "walley_redirect" - ], - "properties": { - "walley_redirect": { - "type": "object", - "description": "For WalleyRedirect as PayLater Option" + "card_network": { + "allOf": [ + { + "$ref": "#/components/schemas/CardNetwork" } - } - }, - { - "type": "object", - "required": [ - "alma_redirect" ], - "properties": { - "alma_redirect": { - "type": "object", - "description": "For Alma Redirection as PayLater Option" + "nullable": true + }, + "bank_transfer": { + "allOf": [ + { + "$ref": "#/components/schemas/Bank" } - } + ], + "nullable": true }, - { + "metadata": { "type": "object", - "required": [ - "atome_redirect" - ], - "properties": { - "atome_redirect": { - "type": "object" - } - } + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true } - ] + } }, - "PayPalWalletData": { + "PaymentMethodsEnabled": { "type": "object", + "description": "Details of all the payment methods enabled for the connector for the given merchant account", "required": [ - "token" + "payment_method" ], "properties": { - "token": { - "type": "string", - "description": "Token generated for the Apple pay" + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RequestPaymentMethodTypes" + }, + "description": "Subtype of payment method", + "example": [ + "credit" + ], + "nullable": true } } }, - "PaymentAttemptResponse": { + "PaymentRetrieveBody": { "type": "object", - "required": [ - "attempt_id", - "status", - "amount" - ], "properties": { - "attempt_id": { + "merchant_id": { "type": "string", - "description": "Unique identifier for the attempt" - }, - "status": { - "$ref": "#/components/schemas/AttemptStatus" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment attempt amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.," + "description": "The identifier for the Merchant Account.", + "nullable": true }, - "currency": { - "allOf": [ - { - "$ref": "#/components/schemas/Currency" - } - ], + "force_sync": { + "type": "boolean", + "description": "Decider to enable or disable the connector call for retrieve request", "nullable": true }, - "connector": { + "client_secret": { "type": "string", - "description": "The connector used for the payment", + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", "nullable": true }, - "error_message": { + "expand_captures": { + "type": "boolean", + "description": "If enabled provides list of captures linked to latest attempt", + "nullable": true + }, + "expand_attempts": { + "type": "boolean", + "description": "If enabled provides list of attempts linked to payment intent", + "nullable": true + } + } + }, + "PaymentType": { + "type": "string", + "description": "To be used to specify the type of payment. Use 'setup_mandate' in case of zero auth flow.", + "enum": [ + "normal", + "new_mandate", + "setup_mandate", + "recurring_mandate" + ] + }, + "PaymentsCancelRequest": { + "type": "object", + "properties": { + "cancellation_reason": { "type": "string", - "description": "If there was an error while calling the connector the error message is received here", + "description": "The reason for the payment cancel", "nullable": true }, - "payment_method": { + "merchant_connector_details": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethod" + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } ], "nullable": true + } + } + }, + "PaymentsCaptureRequest": { + "type": "object", + "properties": { + "merchant_id": { + "type": "string", + "description": "The unique identifier for the merchant", + "nullable": true }, - "connector_transaction_id": { + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured/ debited from the user's payment method.", + "nullable": true + }, + "refund_uncaptured_amount": { + "type": "boolean", + "description": "Decider to refund the uncaptured amount", + "nullable": true + }, + "statement_descriptor_suffix": { "type": "string", - "description": "A unique identifier for a payment provided by the connector", + "description": "Provides information about a card payment that customers see on their statements.", "nullable": true }, - "capture_method": { + "statement_descriptor_prefix": { + "type": "string", + "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", + "nullable": true + }, + "merchant_connector_details": { "allOf": [ { - "$ref": "#/components/schemas/CaptureMethod" + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } ], "nullable": true + } + } + }, + "PaymentsConfirmRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "example": 6540, + "nullable": true, + "minimum": 0 }, - "authentication_type": { + "currency": { "allOf": [ { - "$ref": "#/components/schemas/AuthenticationType" + "$ref": "#/components/schemas/Currency" } ], - "default": "three_ds", "nullable": true }, - "cancellation_reason": { - "type": "string", - "description": "If the payment was cancelled the reason provided here", + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, "nullable": true }, - "mandate_id": { + "payment_id": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", - "nullable": true + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 }, - "error_code": { + "merchant_id": { "type": "string", - "description": "If there was an error while calling the connectors the code is received here", + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "nullable": true, + "maxLength": 255 + }, + "routing": { + "allOf": [ + { + "$ref": "#/components/schemas/StraightThroughAlgorithm" + } + ], "nullable": true }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", + "connector": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connector" + }, + "description": "This allows to manually select a connector with which the payment can go through", + "example": [ + "stripe", + "adyen" + ], "nullable": true }, - "connector_metadata": { - "description": "additional data related to some connectors", + "capture_method": { + "allOf": [ + { + "$ref": "#/components/schemas/CaptureMethod" + } + ], "nullable": true }, - "payment_experience": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "payment_method_type": { + "billing": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/Address" } ], "nullable": true }, - "reference_id": { + "capture_on": { "type": "string", - "description": "reference to the payment at connector side", - "example": "993672945374576J", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "unified_code": { - "type": "string", - "description": "error code unified across the connectors is received here if there was an error while calling connector", + "confirm": { + "type": "boolean", + "description": "Whether to confirm the payment (if applicable)", + "default": false, + "example": true, "nullable": true }, - "unified_message": { - "type": "string", - "description": "error message unified across the connectors is received here if there was an error while calling connector", - "nullable": true - } - } - }, - "PaymentCreatePaymentLinkConfig": { - "allOf": [ - { + "customer": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkConfigRequest" + "$ref": "#/components/schemas/CustomerDetails" } ], "nullable": true }, - { - "type": "object" - } - ] - }, - "PaymentExperience": { - "type": "string", - "enum": [ - "redirect_to_url", - "invoke_sdk_client", - "display_qr_code", - "one_click", - "link_wallet", - "invoke_payment_app", - "display_wait_screen" - ] - }, - "PaymentIdType": { - "oneOf": [ - { - "type": "object", - "required": [ - "PaymentIntentId" - ], - "properties": { - "PaymentIntentId": { - "type": "string", - "description": "The identifier for payment intent" - } - } - }, - { - "type": "object", - "required": [ - "ConnectorTransactionId" - ], - "properties": { - "ConnectorTransactionId": { - "type": "string", - "description": "The identifier for connector transaction" - } - } - }, - { - "type": "object", - "required": [ - "PaymentAttemptId" - ], - "properties": { - "PaymentAttemptId": { - "type": "string", - "description": "The identifier for payment attempt" - } - } - }, - { - "type": "object", - "required": [ - "PreprocessingId" - ], - "properties": { - "PreprocessingId": { - "type": "string", - "description": "The identifier for preprocessing step" - } - } - } - ] - }, - "PaymentLinkConfig": { - "type": "object", - "required": [ - "theme", - "logo", - "seller_name", - "sdk_layout" - ], - "properties": { - "theme": { - "type": "string", - "description": "custom theme for the payment link" - }, - "logo": { - "type": "string", - "description": "merchant display logo" - }, - "seller_name": { - "type": "string", - "description": "Custom merchant name for payment link" - }, - "sdk_layout": { - "type": "string", - "description": "Custom layout for sdk" - } - } - }, - "PaymentLinkConfigRequest": { - "type": "object", - "properties": { - "theme": { + "customer_id": { "type": "string", - "description": "custom theme for the payment link", - "example": "#4E6ADD", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", "nullable": true, "maxLength": 255 }, - "logo": { + "email": { "type": "string", - "description": "merchant display logo", - "example": "https://i.pinimg.com/736x/4d/83/5c/4d835ca8aafbbb15f84d07d926fda473.jpg", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", + "example": "johntest@test.com", "nullable": true, "maxLength": 255 }, - "seller_name": { + "name": { "type": "string", - "description": "Custom merchant name for payment link", - "example": "hyperswitch", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", + "example": "John Test", "nullable": true, "maxLength": 255 }, - "sdk_layout": { + "phone": { "type": "string", - "description": "Custom layout for sdk", - "example": "accordion", + "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "3141592653", "nullable": true, "maxLength": 255 - } - } - }, - "PaymentLinkInitiateRequest": { - "type": "object", - "required": [ - "merchant_id", - "payment_id" - ], - "properties": { - "merchant_id": { - "type": "string" - }, - "payment_id": { - "type": "string" - } - } - }, - "PaymentLinkResponse": { - "type": "object", - "required": [ - "link", - "payment_link_id" - ], - "properties": { - "link": { - "type": "string" - }, - "payment_link_id": { - "type": "string" - } - } - }, - "PaymentLinkStatus": { - "type": "string", - "enum": [ - "active", - "expired" - ] - }, - "PaymentListConstraints": { - "type": "object", - "properties": { - "customer_id": { - "type": "string", - "description": "The identifier for customer", - "example": "cus_meowuwunwiuwiwqw", - "nullable": true - }, - "starting_after": { - "type": "string", - "description": "A cursor for use in pagination, fetch the next list after some object", - "example": "pay_fafa124123", - "nullable": true - }, - "ending_before": { - "type": "string", - "description": "A cursor for use in pagination, fetch the previous list before some object", - "example": "pay_fafa124123", - "nullable": true - }, - "limit": { - "type": "integer", - "format": "int32", - "description": "limit on the number of objects to return", - "default": 10, - "maximum": 100, - "minimum": 0 - }, - "created": { - "type": "string", - "format": "date-time", - "description": "The time at which payment is created", - "example": "2022-09-10T10:11:12Z", - "nullable": true }, - "created.lt": { + "phone_country_code": { "type": "string", - "format": "date-time", - "description": "Time less than the payment created time", - "example": "2022-09-10T10:11:12Z", - "nullable": true + "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "+1", + "nullable": true, + "maxLength": 255 }, - "created.gt": { - "type": "string", - "format": "date-time", - "description": "Time greater than the payment created time", - "example": "2022-09-10T10:11:12Z", + "off_session": { + "type": "boolean", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", + "example": true, "nullable": true }, - "created.lte": { + "description": { "type": "string", - "format": "date-time", - "description": "Time less than or equals to the payment created time", - "example": "2022-09-10T10:11:12Z", + "description": "A description for the payment", + "example": "It's my first payment request", "nullable": true }, - "created.gte": { + "return_url": { "type": "string", - "format": "date-time", - "description": "Time greater than or equals to the payment created time", - "example": "2022-09-10T10:11:12Z", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", "nullable": true - } - } - }, - "PaymentListResponse": { - "type": "object", - "required": [ - "size", - "data" - ], - "properties": { - "size": { - "type": "integer", - "description": "The number of payments included in the list", - "minimum": 0 - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentsResponse" - } - } - } - }, - "PaymentMethod": { - "type": "string", - "enum": [ - "card", - "card_redirect", - "pay_later", - "wallet", - "bank_redirect", - "bank_transfer", - "crypto", - "bank_debit", - "reward", - "upi", - "voucher", - "gift_card" - ] - }, - "PaymentMethodCreate": { - "type": "object", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" }, - "payment_method_type": { + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/FutureUsage" } ], "nullable": true }, - "payment_method_issuer": { - "type": "string", - "description": "The name of the bank/ provider issuing the payment method to the end user", - "example": "Citibank", + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodData" + } + ], "nullable": true }, - "payment_method_issuer_code": { + "payment_method": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodIssuerCode" + "$ref": "#/components/schemas/PaymentMethod" } ], "nullable": true }, - "card": { + "payment_token": { + "type": "string", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", + "nullable": true + }, + "card_cvc": { + "type": "string", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, + "nullable": true + }, + "shipping": { "allOf": [ { - "$ref": "#/components/schemas/CardDetail" + "$ref": "#/components/schemas/Address" } ], "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true + "statement_descriptor_name": { + "type": "string", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 255 }, - "customer_id": { + "statement_descriptor_suffix": { "type": "string", - "description": "The unique identifier of the customer.", - "example": "cus_meowerunwiuwiwqw", + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 + }, + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "card_network": { + "client_secret": { "type": "string", - "description": "The card network", - "example": "Visa", + "description": "It's a token used for client side verification.", + "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", "nullable": true }, - "bank_transfer": { + "mandate_data": { "allOf": [ { - "$ref": "#/components/schemas/Bank" + "$ref": "#/components/schemas/MandateData" } ], "nullable": true - } - } - }, - "PaymentMethodData": { - "oneOf": [ - { - "type": "object", - "required": [ - "card" - ], - "properties": { - "card": { - "$ref": "#/components/schemas/Card" - } - } }, - { - "type": "object", - "required": [ - "card_redirect" - ], - "properties": { - "card_redirect": { - "$ref": "#/components/schemas/CardRedirectData" - } - } + "mandate_id": { + "type": "string", + "description": "A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data", + "example": "mandate_iwer89rnjef349dni3", + "nullable": true, + "maxLength": 255 }, - { - "type": "object", - "required": [ - "wallet" - ], - "properties": { - "wallet": { - "$ref": "#/components/schemas/WalletData" + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" } - } - }, - { - "type": "object", - "required": [ - "pay_later" ], - "properties": { - "pay_later": { - "$ref": "#/components/schemas/PayLaterData" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "bank_redirect" - ], - "properties": { - "bank_redirect": { - "$ref": "#/components/schemas/BankRedirectData" + "payment_experience": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentExperience" } - } - }, - { - "type": "object", - "required": [ - "bank_debit" ], - "properties": { - "bank_debit": { - "$ref": "#/components/schemas/BankDebitData" - } - } + "nullable": true }, - { - "type": "object", - "required": [ - "bank_transfer" - ], - "properties": { - "bank_transfer": { - "$ref": "#/components/schemas/BankTransferData" + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" } - } - }, - { - "type": "object", - "required": [ - "crypto" ], - "properties": { - "crypto": { - "$ref": "#/components/schemas/CryptoData" - } - } - }, - { - "type": "string", - "enum": [ - "mandate_payment" - ] - }, - { - "type": "string", - "enum": [ - "reward" - ] + "nullable": true }, - { - "type": "object", - "required": [ - "upi" - ], - "properties": { - "upi": { - "$ref": "#/components/schemas/UpiData" + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } - } - }, - { - "type": "object", - "required": [ - "voucher" ], - "properties": { - "voucher": { - "$ref": "#/components/schemas/VoucherData" + "nullable": true + }, + "allowed_payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", + "nullable": true + }, + "retry_action": { + "allOf": [ + { + "$ref": "#/components/schemas/RetryAction" } - } + ], + "nullable": true }, - { + "metadata": { "type": "object", - "required": [ - "gift_card" + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "connector_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorMetadata" + } ], - "properties": { - "gift_card": { - "$ref": "#/components/schemas/GiftCardData" + "nullable": true + }, + "feature_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/FeatureMetadata" } - } + ], + "nullable": true }, - { - "type": "object", - "required": [ - "card_token" + "payment_link": { + "type": "boolean", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" + } ], - "properties": { - "card_token": { - "$ref": "#/components/schemas/CardToken" + "nullable": true + }, + "payment_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentType" } - } - } - ] - }, - "PaymentMethodDeleteResponse": { - "type": "object", - "required": [ - "payment_method_id", - "deleted" - ], - "properties": { - "payment_method_id": { - "type": "string", - "description": "The unique identifier of the Payment method", - "example": "card_rGK4Vi5iSW70MY7J2mIy" + ], + "nullable": true }, - "deleted": { + "request_incremental_authorization": { "type": "boolean", - "description": "Whether payment method was deleted or not", - "example": true + "description": "Request for an incremental authorization", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, - "PaymentMethodIssuerCode": { - "type": "string", - "enum": [ - "jp_hdfc", - "jp_icici", - "jp_googlepay", - "jp_applepay", - "jp_phonepay", - "jp_wechat", - "jp_sofort", - "jp_giropay", - "jp_sepa", - "jp_bacs" - ] - }, - "PaymentMethodList": { + "PaymentsCreateRequest": { "type": "object", "required": [ - "payment_method" + "amount", + "currency" ], "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "minimum": 0 }, - "payment_method_types": { + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, + "nullable": true + }, + "payment_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "merchant_id": { + "type": "string", + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "nullable": true, + "maxLength": 255 + }, + "routing": { + "allOf": [ + { + "$ref": "#/components/schemas/StraightThroughAlgorithm" + } + ], + "nullable": true + }, + "connector": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/Connector" }, - "description": "This is a sub-category of payment method.", + "description": "This allows to manually select a connector with which the payment can go through", "example": [ - "credit" + "stripe", + "adyen" ], "nullable": true - } - } - }, - "PaymentMethodListResponse": { - "type": "object", - "required": [ - "payment_methods", - "mandate_payment", - "show_surcharge_breakup_screen" - ], - "properties": { - "redirect_url": { + }, + "capture_method": { + "allOf": [ + { + "$ref": "#/components/schemas/CaptureMethod" + } + ], + "nullable": true + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "default": "three_ds", + "nullable": true + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "capture_on": { "type": "string", - "description": "Redirect URL of the merchant", - "example": "https://www.google.com", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", "nullable": true }, - "payment_methods": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodList" - }, - "description": "Information about the payment method", - "example": [ + "confirm": { + "type": "boolean", + "description": "Whether to confirm the payment (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "customer": { + "allOf": [ { - "payment_method": "wallet", - "payment_experience": null, - "payment_method_issuers": [ - "labore magna ipsum", - "aute" - ] + "$ref": "#/components/schemas/CustomerDetails" } - ] + ], + "nullable": true + }, + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", + "example": "johntest@test.com", + "nullable": true, + "maxLength": 255 }, - "mandate_payment": { - "$ref": "#/components/schemas/MandateType" + "name": { + "type": "string", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", + "example": "John Test", + "nullable": true, + "maxLength": 255 }, - "merchant_name": { + "phone": { "type": "string", - "nullable": true + "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "3141592653", + "nullable": true, + "maxLength": 255 }, - "show_surcharge_breakup_screen": { + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "+1", + "nullable": true, + "maxLength": 255 + }, + "off_session": { "type": "boolean", - "description": "flag to indicate if surcharge and tax breakup screen should be shown or not" + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", + "example": true, + "nullable": true }, - "payment_type": { + "description": { + "type": "string", + "description": "A description for the payment", + "example": "It's my first payment request", + "nullable": true + }, + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", + "nullable": true + }, + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/PaymentType" + "$ref": "#/components/schemas/FutureUsage" } ], "nullable": true - } - } - }, - "PaymentMethodResponse": { - "type": "object", - "required": [ - "merchant_id", - "payment_method_id", - "payment_method", - "recurring_enabled", - "installment_payment_enabled" - ], - "properties": { - "merchant_id": { - "type": "string", - "description": "Unique identifier for a merchant", - "example": "merchant_1671528864" }, - "customer_id": { - "type": "string", - "description": "The unique identifier of the customer.", - "example": "cus_meowerunwiuwiwqw", + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodData" + } + ], "nullable": true }, - "payment_method_id": { - "type": "string", - "description": "The unique identifier of the Payment method", - "example": "card_rGK4Vi5iSW70MY7J2mIy" - }, "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "payment_method_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/PaymentMethod" } ], "nullable": true }, - "card": { + "payment_token": { + "type": "string", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", + "nullable": true + }, + "card_cvc": { + "type": "string", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, + "nullable": true + }, + "shipping": { "allOf": [ { - "$ref": "#/components/schemas/CardDetailFromLocker" + "$ref": "#/components/schemas/Address" } ], "nullable": true }, - "recurring_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for recurring payments", - "example": true + "statement_descriptor_name": { + "type": "string", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 255 }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method is eligible for installment payments", - "example": true + "statement_descriptor_suffix": { + "type": "string", + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 }, - "payment_experience": { + "order_details": { "type": "array", "items": { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/OrderDetailsWithAmount" }, - "description": "Type of payment experience enabled with the connector", - "example": [ - "redirect_to_url" - ], + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "mandate_data": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateData" + } + ], "nullable": true }, - "created": { + "mandate_id": { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the customer was created", - "example": "2023-01-18T11:04:09.922Z", + "description": "A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data", + "example": "mandate_iwer89rnjef349dni3", + "nullable": true, + "maxLength": 255 + }, + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true }, - "bank_transfer": { + "payment_experience": { "allOf": [ { - "$ref": "#/components/schemas/Bank" + "$ref": "#/components/schemas/PaymentExperience" } ], "nullable": true - } - } - }, - "PaymentMethodType": { - "type": "string", - "enum": [ - "ach", - "affirm", - "afterpay_clearpay", - "alfamart", - "ali_pay", - "ali_pay_hk", - "alma", - "apple_pay", - "atome", - "bacs", - "bancontact_card", - "becs", - "benefit", - "bizum", - "blik", - "boleto", - "bca_bank_transfer", - "bni_va", - "bri_va", - "card_redirect", - "cimb_va", - "classic", - "credit", - "crypto_currency", - "cashapp", - "dana", - "danamon_va", - "debit", - "efecty", - "eps", - "evoucher", - "giropay", - "givex", - "google_pay", - "go_pay", - "gcash", - "ideal", - "interac", - "indomaret", - "klarna", - "kakao_pay", - "mandiri_va", - "knet", - "mb_way", - "mobile_pay", - "momo", - "momo_atm", - "multibanco", - "online_banking_thailand", - "online_banking_czech_republic", - "online_banking_finland", - "online_banking_fpx", - "online_banking_poland", - "online_banking_slovakia", - "oxxo", - "pago_efectivo", - "permata_bank_transfer", - "open_banking_uk", - "pay_bright", - "paypal", - "pix", - "pay_safe_card", - "przelewy24", - "pse", - "red_compra", - "red_pagos", - "samsung_pay", - "sepa", - "sofort", - "swish", - "touch_n_go", - "trustly", - "twint", - "upi_collect", - "vipps", - "walley", - "we_chat_pay", - "seven_eleven", - "lawson", - "mini_stop", - "family_mart", - "seicomart", - "pay_easy" - ] - }, - "PaymentMethodUpdate": { - "type": "object", - "properties": { - "card": { + }, + "payment_method_type": { "allOf": [ { - "$ref": "#/components/schemas/CardDetail" + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + }, + "business_country": { + "allOf": [ + { + "$ref": "#/components/schemas/CountryAlpha2" } ], "nullable": true }, - "card_network": { + "business_label": { + "type": "string", + "description": "Business label of the merchant for this payment.\nTo be deprecated soon. Pass the profile_id instead", + "example": "food", + "nullable": true + }, + "merchant_connector_details": { "allOf": [ { - "$ref": "#/components/schemas/CardNetwork" + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } ], "nullable": true }, - "bank_transfer": { + "allowed_payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", + "nullable": true + }, + "retry_action": { "allOf": [ { - "$ref": "#/components/schemas/Bank" + "$ref": "#/components/schemas/RetryAction" } ], "nullable": true @@ -9704,133 +12639,123 @@ "type": "object", "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true - } - } - }, - "PaymentMethodsEnabled": { - "type": "object", - "description": "Details of all the payment methods enabled for the connector for the given merchant account", - "required": [ - "payment_method" - ], - "properties": { - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" }, - "payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RequestPaymentMethodTypes" - }, - "description": "Subtype of payment method", - "example": [ - "credit" + "connector_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorMetadata" + } ], "nullable": true - } - } - }, - "PaymentRetrieveBody": { - "type": "object", - "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", + }, + "feature_metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/FeatureMetadata" + } + ], "nullable": true }, - "force_sync": { + "payment_link": { "type": "boolean", - "description": "Decider to enable or disable the connector call for retrieve request", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, "nullable": true }, - "client_secret": { + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" + } + ], + "nullable": true + }, + "profile_id": { "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", + "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", "nullable": true }, - "expand_captures": { - "type": "boolean", - "description": "If enabled provides list of captures linked to latest attempt", + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/RequestSurchargeDetails" + } + ], "nullable": true }, - "expand_attempts": { + "payment_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentType" + } + ], + "nullable": true + }, + "request_incremental_authorization": { "type": "boolean", - "description": "If enabled provides list of attempts linked to payment intent", + "description": "Request for an incremental authorization", + "nullable": true + }, + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", "nullable": true } } }, - "PaymentType": { - "type": "string", - "enum": [ - "normal", - "new_mandate", - "setup_mandate", - "recurring_mandate" - ] - }, - "PaymentsCancelRequest": { + "PaymentsIncrementalAuthorizationRequest": { "type": "object", "required": [ - "merchant_connector_details" + "amount" ], "properties": { - "cancellation_reason": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The total amount including previously authorized amount and additional amount", + "example": 6540 + }, + "reason": { "type": "string", - "description": "The reason for the payment cancel", + "description": "Reason for incremental authorization", "nullable": true - }, - "merchant_connector_details": { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } } }, - "PaymentsCaptureRequest": { + "PaymentsRequest": { "type": "object", "properties": { - "merchant_id": { - "type": "string", - "description": "The unique identifier for the merchant", - "nullable": true - }, - "amount_to_capture": { + "amount": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", - "nullable": true - }, - "refund_uncaptured_amount": { - "type": "boolean", - "description": "Decider to refund the uncaptured amount", - "nullable": true - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements.", - "nullable": true - }, - "statement_descriptor_prefix": { - "type": "string", - "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", - "nullable": true + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "example": 6540, + "nullable": true, + "minimum": 0 }, - "merchant_connector_details": { + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + "$ref": "#/components/schemas/Currency" } ], "nullable": true - } - } - }, - "PaymentsCreateRequest": { - "type": "object", - "required": [ - "amount", - "currency" - ], - "properties": { + }, + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, + "nullable": true + }, "payment_id": { "type": "string", "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", @@ -9846,18 +12771,10 @@ "nullable": true, "maxLength": 255 }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540, - "nullable": true, - "minimum": 0 - }, "routing": { "allOf": [ { - "$ref": "#/components/schemas/RoutingAlgorithm" + "$ref": "#/components/schemas/StraightThroughAlgorithm" } ], "nullable": true @@ -9867,34 +12784,36 @@ "items": { "$ref": "#/components/schemas/Connector" }, - "description": "This allows the merchant to manually select a connector with which the payment can go through", + "description": "This allows to manually select a connector with which the payment can go through", "example": [ "stripe", "adyen" ], "nullable": true }, - "currency": { + "capture_method": { "allOf": [ { - "$ref": "#/components/schemas/Currency" + "$ref": "#/components/schemas/CaptureMethod" } ], "nullable": true }, - "capture_method": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/CaptureMethod" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "amount_to_capture": { - "type": "integer", - "format": "int64", - "description": "The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,\nIf not provided, the default amount_to_capture will be the payment amount.", - "example": 6540, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], "nullable": true }, "capture_on": { @@ -9921,21 +12840,21 @@ }, "customer_id": { "type": "string", - "description": "The identifier for the customer object.\nThis field will be deprecated soon, use the customer object instead", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", "nullable": true, "maxLength": 255 }, "email": { "type": "string", - "description": "The customer's email address\nThis field will be deprecated soon, use the customer object instead", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", "example": "johntest@test.com", "nullable": true, "maxLength": 255 }, "name": { "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon, use the customer object instead", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", "example": "John Test", "nullable": true, "maxLength": 255 @@ -9956,13 +12875,13 @@ }, "off_session": { "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`.", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", "example": true, "nullable": true }, "description": { "type": "string", - "description": "A description of the payment", + "description": "A description for the payment", "example": "It's my first payment request", "nullable": true }, @@ -9980,15 +12899,6 @@ ], "nullable": true }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true - }, "payment_method_data": { "allOf": [ { @@ -10013,7 +12923,7 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", "deprecated": true, "nullable": true }, @@ -10025,14 +12935,6 @@ ], "nullable": true }, - "billing": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, "statement_descriptor_name": { "type": "string", "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", @@ -10052,8 +12954,8 @@ "items": { "$ref": "#/components/schemas/OrderDetailsWithAmount" }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, "client_secret": { @@ -10072,14 +12974,17 @@ }, "mandate_id": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", + "description": "A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data", "example": "mandate_iwer89rnjef349dni3", "nullable": true, "maxLength": 255 }, "browser_info": { - "type": "object", - "description": "Additional details required by 3DS 2.0", + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true }, "payment_experience": { @@ -10108,7 +13013,7 @@ }, "business_label": { "type": "string", - "description": "Business label of the merchant for this payment", + "description": "Business label of the merchant for this payment.\nTo be deprecated soon. Pass the profile_id instead", "example": "food", "nullable": true }, @@ -10125,7 +13030,7 @@ "items": { "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "Allowed Payment Method Types for a given PaymentIntent", + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", "nullable": true }, "business_sub_label": { @@ -10217,12 +13122,20 @@ } } }, - "PaymentsRequest": { + "PaymentsResponse": { "type": "object", + "required": [ + "status", + "amount", + "net_amount", + "currency", + "payment_method", + "attempt_count" + ], "properties": { "payment_id": { "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", "example": "pay_mbabizu24mvu3mela5njyhpit4", "nullable": true, "maxLength": 30, @@ -10235,158 +13148,157 @@ "nullable": true, "maxLength": 255 }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540, - "nullable": true, - "minimum": 0 - }, - "routing": { + "status": { "allOf": [ { - "$ref": "#/components/schemas/RoutingAlgorithm" + "$ref": "#/components/schemas/IntentStatus" } ], - "nullable": true + "default": "requires_confirmation" }, - "connector": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Connector" - }, - "description": "This allows the merchant to manually select a connector with which the payment can go through", - "example": [ - "stripe", - "adyen" - ], - "nullable": true + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", + "example": 100 }, - "currency": { - "allOf": [ - { - "$ref": "#/components/schemas/Currency" - } - ], - "nullable": true + "net_amount": { + "type": "integer", + "format": "int64", + "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount,\nIf no surcharge_details, net_amount = amount", + "example": 110 }, - "capture_method": { - "allOf": [ - { - "$ref": "#/components/schemas/CaptureMethod" - } - ], - "nullable": true + "amount_capturable": { + "type": "integer", + "format": "int64", + "description": "The maximum amount that could be captured from the payment", + "example": 6540, + "nullable": true, + "minimum": 100 }, - "amount_to_capture": { + "amount_received": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,\nIf not provided, the default amount_to_capture will be the payment amount.", + "description": "The amount which is already captured from the payment", "example": 6540, - "nullable": true + "nullable": true, + "minimum": 100 }, - "capture_on": { + "connector": { "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", - "example": "2022-09-10T10:11:12Z", + "description": "The connector used for the payment", + "example": "stripe", "nullable": true }, - "confirm": { - "type": "boolean", - "description": "Whether to confirm the payment (if applicable)", - "default": false, - "example": true, + "client_secret": { + "type": "string", + "description": "It's a token used for client side verification.", + "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", "nullable": true }, - "customer": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerDetails" - } - ], + "created": { + "type": "string", + "format": "date-time", + "description": "Time when the payment was created", + "example": "2022-09-10T10:11:12Z", "nullable": true }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, "customer_id": { "type": "string", - "description": "The identifier for the customer object.\nThis field will be deprecated soon, use the customer object instead", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", "nullable": true, "maxLength": 255 }, - "email": { - "type": "string", - "description": "The customer's email address\nThis field will be deprecated soon, use the customer object instead", - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 - }, - "name": { + "description": { "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon, use the customer object instead", - "example": "John Test", - "nullable": true, - "maxLength": 255 + "description": "A description of the payment", + "example": "It's my first payment request", + "nullable": true }, - "phone": { - "type": "string", - "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", - "example": "3141592653", - "nullable": true, - "maxLength": 255 + "refunds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefundResponse" + }, + "description": "List of refund that happened on this intent", + "nullable": true }, - "phone_country_code": { - "type": "string", - "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", - "example": "+1", - "nullable": true, - "maxLength": 255 + "disputes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" + }, + "description": "List of dispute that happened on this intent", + "nullable": true }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with `confirm: true`.", - "example": true, + "attempts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentAttemptResponse" + }, + "description": "List of attempts that happened on this intent", "nullable": true }, - "description": { - "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", + "captures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaptureResponse" + }, + "description": "List of captures done on latest attempt", "nullable": true }, - "return_url": { + "mandate_id": { "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io", - "nullable": true + "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", + "example": "mandate_iwer89rnjef349dni3", + "nullable": true, + "maxLength": 255 }, - "setup_future_usage": { + "mandate_data": { "allOf": [ { - "$ref": "#/components/schemas/FutureUsage" + "$ref": "#/components/schemas/MandateData" } ], "nullable": true }, - "authentication_type": { + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/AuthenticationType" + "$ref": "#/components/schemas/FutureUsage" } ], - "default": "three_ds", "nullable": true }, - "payment_method_data": { + "off_session": { + "type": "boolean", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", + "example": true, + "nullable": true + }, + "capture_on": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "capture_method": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodData" + "$ref": "#/components/schemas/CaptureMethod" } ], "nullable": true }, "payment_method": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_method_data": { "allOf": [ { "$ref": "#/components/schemas/PaymentMethod" @@ -10400,12 +13312,6 @@ "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true }, - "card_cvc": { - "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", - "deprecated": true, - "nullable": true - }, "shipping": { "allOf": [ { @@ -10422,6 +13328,51 @@ ], "nullable": true }, + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", + "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", + "nullable": true + }, + "email": { + "type": "string", + "description": "description: The customer's email address", + "example": "johntest@test.com", + "nullable": true, + "maxLength": 255 + }, + "name": { + "type": "string", + "description": "description: The customer's name", + "example": "John Test", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "3141592653", + "nullable": true, + "maxLength": 255 + }, + "return_url": { + "type": "string", + "description": "The URL to redirect after the completion of the operation", + "example": "https://hyperswitch.io", + "nullable": true + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "default": "three_ds", + "nullable": true + }, "statement_descriptor_name": { "type": "string", "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", @@ -10431,44 +13382,44 @@ }, "statement_descriptor_suffix": { "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", "example": "Payment for shoes purchase", "nullable": true, "maxLength": 255 }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/NextActionData" + } + ], + "nullable": true + }, + "cancellation_reason": { + "type": "string", + "description": "If the payment was cancelled the reason provided here", + "nullable": true + }, + "error_code": { + "type": "string", + "description": "If there was an error while calling the connectors the code is received here", + "example": "E0001", "nullable": true }, - "client_secret": { + "error_message": { "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", + "description": "If there was an error while calling the connector the error message is received here", + "example": "Failed while verifying the card", "nullable": true }, - "mandate_data": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateData" - } - ], + "unified_code": { + "type": "string", + "description": "error code unified across the connectors is received here if there was an error while calling connector", "nullable": true }, - "mandate_id": { + "unified_message": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", - "example": "mandate_iwer89rnjef349dni3", - "nullable": true, - "maxLength": 255 - }, - "browser_info": { - "type": "object", - "description": "Additional details required by 3DS 2.0", + "description": "error message unified across the connectors is received here if there was an error while calling connector", "nullable": true }, "payment_experience": { @@ -10487,6 +13438,12 @@ ], "nullable": true }, + "connector_label": { + "type": "string", + "description": "The connector used for this payment along with the country and business details", + "example": "stripe_US_food", + "nullable": true + }, "business_country": { "allOf": [ { @@ -10497,16 +13454,12 @@ }, "business_label": { "type": "string", - "description": "Business label of the merchant for this payment", - "example": "food", + "description": "The business label of merchant for this payment", "nullable": true }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], + "business_sub_label": { + "type": "string", + "description": "The business_sub_label for this payment", "nullable": true }, "allowed_payment_method_types": { @@ -10517,15 +13470,29 @@ "description": "Allowed Payment Method Types for a given PaymentIntent", "nullable": true }, - "business_sub_label": { + "ephemeral_key": { + "allOf": [ + { + "$ref": "#/components/schemas/EphemeralKeyCreateResponse" + } + ], + "nullable": true + }, + "manual_retry_allowed": { + "type": "boolean", + "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", + "nullable": true + }, + "connector_transaction_id": { "type": "string", - "description": "Business sub label for the payment", + "description": "A unique identifier for a payment provided by the connector", + "example": "993672945374576J", "nullable": true }, - "retry_action": { + "frm_message": { "allOf": [ { - "$ref": "#/components/schemas/RetryAction" + "$ref": "#/components/schemas/FrmMessage" } ], "nullable": true @@ -10551,24 +13518,23 @@ ], "nullable": true }, - "payment_link": { - "type": "boolean", - "description": "Whether to get the payment link (if applicable)", - "default": false, - "example": true, + "reference_id": { + "type": "string", + "description": "reference to the payment at connector side", + "example": "993672945374576J", "nullable": true }, - "payment_link_config": { + "payment_link": { "allOf": [ { - "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" + "$ref": "#/components/schemas/PaymentLinkResponse" } ], "nullable": true }, "profile_id": { "type": "string", - "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "description": "The business profile that is associated with this payment", "nullable": true }, "surcharge_details": { @@ -10579,196 +13545,247 @@ ], "nullable": true }, - "payment_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentType" - } - ], + "attempt_count": { + "type": "integer", + "format": "int32", + "description": "total number of attempts associated with this payment" + }, + "merchant_decision": { + "type": "string", + "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", "nullable": true }, - "request_incremental_authorization": { + "merchant_connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "nullable": true + }, + "incremental_authorization_allowed": { "type": "boolean", - "description": "Request for an incremental authorization", + "description": "If true incremental authorization can be performed on this payment", "nullable": true }, - "session_expiry": { + "authorization_count": { "type": "integer", "format": "int32", - "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", - "example": 900, - "nullable": true, - "minimum": 0 + "description": "Total number of authorizations happened in an incremental_authorization payment", + "nullable": true }, - "frm_metadata": { - "description": "additional data related to some frm connectors", + "incremental_authorizations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncrementalAuthorizationResponse" + }, + "description": "List of incremental authorizations happened to the payment", + "nullable": true + }, + "expires_on": { + "type": "string", + "format": "date-time", + "description": "Date Time expiry of the payment", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "fingerprint": { + "type": "string", + "description": "Payment Fingerprint", "nullable": true } } }, - "PaymentsResponse": { + "PaymentsRetrieveRequest": { "type": "object", "required": [ - "status", - "amount", - "net_amount", - "currency", - "payment_method", - "attempt_count" + "resource_id", + "force_sync" ], "properties": { - "payment_id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "nullable": true, - "maxLength": 30, - "minLength": 30 + "resource_id": { + "$ref": "#/components/schemas/PaymentIdType" }, "merchant_id": { "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "nullable": true, - "maxLength": 255 - }, - "status": { - "allOf": [ - { - "$ref": "#/components/schemas/IntentStatus" - } - ], - "default": "requires_confirmation" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 100 - }, - "net_amount": { - "type": "integer", - "format": "int64", - "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount,\nIf no surcharge_details, net_amount = amount", - "example": 110 + "description": "The identifier for the Merchant Account.", + "nullable": true }, - "amount_capturable": { - "type": "integer", - "format": "int64", - "description": "The maximum amount that could be captured from the payment", - "example": 6540, - "nullable": true, - "minimum": 100 + "force_sync": { + "type": "boolean", + "description": "Decider to enable or disable the connector call for retrieve request" }, - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount which is already captured from the payment", - "example": 6540, - "nullable": true, - "minimum": 100 + "param": { + "type": "string", + "description": "The parameters passed to a retrieve request", + "nullable": true }, "connector": { "type": "string", - "description": "The connector used for the payment", - "example": "stripe", + "description": "The name of the connector", "nullable": true }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true }, - "created": { + "client_secret": { "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z", + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", "nullable": true }, - "currency": { - "$ref": "#/components/schemas/Currency" + "expand_captures": { + "type": "boolean", + "description": "If enabled provides list of captures linked to latest attempt", + "nullable": true }, - "customer_id": { + "expand_attempts": { + "type": "boolean", + "description": "If enabled provides list of attempts linked to payment intent", + "nullable": true + } + } + }, + "PaymentsSessionRequest": { + "type": "object", + "required": [ + "payment_id", + "client_secret", + "wallets" + ], + "properties": { + "payment_id": { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 255 + "description": "The identifier for the payment" }, - "description": { + "client_secret": { "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", - "nullable": true - }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "List of refund that happened on this intent", - "nullable": true + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" }, - "disputes": { + "wallets": { "type": "array", "items": { - "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" + "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "List of dispute that happened on this intent", - "nullable": true + "description": "The list of the supported wallets" }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentAttemptResponse" - }, - "description": "List of attempts that happened on this intent", + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true + } + } + }, + "PaymentsSessionResponse": { + "type": "object", + "required": [ + "payment_id", + "client_secret", + "session_token" + ], + "properties": { + "payment_id": { + "type": "string", + "description": "The identifier for the payment" }, - "captures": { + "client_secret": { + "type": "string", + "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" + }, + "session_token": { "type": "array", "items": { - "$ref": "#/components/schemas/CaptureResponse" + "$ref": "#/components/schemas/SessionToken" }, - "description": "List of captures done on latest attempt", - "nullable": true + "description": "The list of session token object" + } + } + }, + "PaymentsStartRequest": { + "type": "object", + "required": [ + "payment_id", + "merchant_id", + "attempt_id" + ], + "properties": { + "payment_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response." }, - "mandate_id": { + "merchant_id": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be use instead of payment_method_data", - "example": "mandate_iwer89rnjef349dni3", + "description": "The identifier for the Merchant Account." + }, + "attempt_id": { + "type": "string", + "description": "The identifier for the payment transaction" + } + } + }, + "PaymentsUpdateRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and ¥100 since ¥ is a zero-decimal currency", + "example": 6540, "nullable": true, - "maxLength": 255 + "minimum": 0 }, - "mandate_data": { + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MandateData" + "$ref": "#/components/schemas/Currency" } ], "nullable": true }, - "setup_future_usage": { + "amount_to_capture": { + "type": "integer", + "format": "int64", + "description": "The Amount to be captured / debited from the users payment method. It shall be in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, the default amount_to_capture will be the payment amount.", + "example": 6540, + "nullable": true + }, + "payment_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "merchant_id": { + "type": "string", + "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", + "example": "merchant_1668273825", + "nullable": true, + "maxLength": 255 + }, + "routing": { "allOf": [ { - "$ref": "#/components/schemas/FutureUsage" + "$ref": "#/components/schemas/StraightThroughAlgorithm" } ], "nullable": true }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", - "example": true, - "nullable": true - }, - "capture_on": { - "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", - "example": "2022-09-10T10:11:12Z", + "connector": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Connector" + }, + "description": "This allows to manually select a connector with which the payment can go through", + "example": [ + "stripe", + "adyen" + ], "nullable": true }, "capture_method": { @@ -10779,24 +13796,16 @@ ], "nullable": true }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "payment_method_data": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethod" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "three_ds", "nullable": true }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", - "nullable": true - }, - "shipping": { + "billing": { "allOf": [ { "$ref": "#/components/schemas/Address" @@ -10804,106 +13813,162 @@ ], "nullable": true }, - "billing": { + "capture_on": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", + "example": "2022-09-10T10:11:12Z", + "nullable": true + }, + "confirm": { + "type": "boolean", + "description": "Whether to confirm the payment (if applicable)", + "default": false, + "example": true, + "nullable": true + }, + "customer": { "allOf": [ { - "$ref": "#/components/schemas/Address" + "$ref": "#/components/schemas/CustomerDetails" } ], "nullable": true }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", - "nullable": true + "customer_id": { + "type": "string", + "description": "The identifier for the customer object. This field will be deprecated soon, use the customer object instead", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 255 }, "email": { "type": "string", - "description": "description: The customer's email address", + "description": "The customer's email address This field will be deprecated soon, use the customer object instead", "example": "johntest@test.com", "nullable": true, "maxLength": 255 }, "name": { "type": "string", - "description": "description: The customer's name", + "description": "The customer's name.\nThis field will be deprecated soon, use the customer object instead.", "example": "John Test", "nullable": true, "maxLength": 255 }, "phone": { "type": "string", - "description": "The customer's phone number", + "description": "The customer's phone number\nThis field will be deprecated soon, use the customer object instead", "example": "3141592653", "nullable": true, "maxLength": 255 }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number\nThis field will be deprecated soon, use the customer object instead", + "example": "+1", + "nullable": true, + "maxLength": 255 + }, + "off_session": { + "type": "boolean", + "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. When making a recurring payment by passing a mandate_id, this parameter is mandatory", + "example": true, + "nullable": true + }, + "description": { + "type": "string", + "description": "A description for the payment", + "example": "It's my first payment request", + "nullable": true + }, "return_url": { "type": "string", "description": "The URL to redirect after the completion of the operation", "example": "https://hyperswitch.io", "nullable": true }, - "authentication_type": { + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/AuthenticationType" + "$ref": "#/components/schemas/FutureUsage" } ], - "default": "three_ds", "nullable": true }, - "statement_descriptor_name": { - "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", - "nullable": true, - "maxLength": 255 - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", - "nullable": true, - "maxLength": 255 + "payment_method_data": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodData" + } + ], + "nullable": true }, - "next_action": { + "payment_method": { "allOf": [ { - "$ref": "#/components/schemas/NextActionData" + "$ref": "#/components/schemas/PaymentMethod" } ], "nullable": true }, - "cancellation_reason": { + "payment_token": { "type": "string", - "description": "If the payment was cancelled the reason provided here", + "description": "Provide a reference to a stored payment method", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true }, - "error_code": { + "card_cvc": { "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", + "description": "This is used along with the payment_token field while collecting during saved card payments. This field will be deprecated soon, use the payment_method_data.card_token object instead", + "deprecated": true, "nullable": true }, - "error_message": { + "shipping": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "statement_descriptor_name": { "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 255 + }, + "statement_descriptor_suffix": { + "type": "string", + "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", + "example": "Payment for shoes purchase", + "nullable": true, + "maxLength": 255 + }, + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "unified_code": { - "type": "string", - "description": "error code unified across the connectors is received here if there was an error while calling connector", + "mandate_data": { + "allOf": [ + { + "$ref": "#/components/schemas/MandateData" + } + ], "nullable": true }, - "unified_message": { - "type": "string", - "description": "error message unified across the connectors is received here if there was an error while calling connector", + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true }, "payment_experience": { @@ -10922,61 +13987,26 @@ ], "nullable": true }, - "connector_label": { - "type": "string", - "description": "The connector used for this payment along with the country and business details", - "example": "stripe_US_food", - "nullable": true - }, - "business_country": { + "merchant_connector_details": { "allOf": [ { - "$ref": "#/components/schemas/CountryAlpha2" + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" } ], "nullable": true }, - "business_label": { - "type": "string", - "description": "The business label of merchant for this payment", - "nullable": true - }, - "business_sub_label": { - "type": "string", - "description": "The business_sub_label for this payment", - "nullable": true - }, "allowed_payment_method_types": { "type": "array", "items": { "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "Allowed Payment Method Types for a given PaymentIntent", - "nullable": true - }, - "ephemeral_key": { - "allOf": [ - { - "$ref": "#/components/schemas/EphemeralKeyCreateResponse" - } - ], - "nullable": true - }, - "manual_retry_allowed": { - "type": "boolean", - "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", - "nullable": true - }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", "nullable": true }, - "frm_message": { + "retry_action": { "allOf": [ { - "$ref": "#/components/schemas/FrmMessage" + "$ref": "#/components/schemas/RetryAction" } ], "nullable": true @@ -11002,25 +14032,21 @@ ], "nullable": true }, - "reference_id": { - "type": "string", - "description": "reference to the payment at connector side", - "example": "993672945374576J", + "payment_link": { + "type": "boolean", + "description": "Whether to get the payment link (if applicable)", + "default": false, + "example": true, "nullable": true }, - "payment_link": { + "payment_link_config": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkResponse" + "$ref": "#/components/schemas/PaymentCreatePaymentLinkConfig" } ], "nullable": true }, - "profile_id": { - "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true - }, "surcharge_details": { "allOf": [ { @@ -11029,185 +14055,30 @@ ], "nullable": true }, - "attempt_count": { - "type": "integer", - "format": "int32", - "description": "total number of attempts associated with this payment" - }, - "merchant_decision": { - "type": "string", - "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", - "nullable": true - }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", - "nullable": true - }, - "incremental_authorization_allowed": { - "type": "boolean", - "description": "If true incremental authorization can be performed on this payment", - "nullable": true - }, - "authorization_count": { - "type": "integer", - "format": "int32", - "description": "Total number of authorizations happened in an incremental_authorization payment", - "nullable": true - }, - "incremental_authorizations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncrementalAuthorizationResponse" - }, - "description": "List of incremental authorizations happened to the payment", - "nullable": true - }, - "expires_on": { - "type": "string", - "format": "date-time", - "description": "Date Time expiry of the payment", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "fingerprint": { - "type": "string", - "description": "Payment Fingerprint", - "nullable": true - } - } - }, - "PaymentsRetrieveRequest": { - "type": "object", - "required": [ - "resource_id", - "force_sync" - ], - "properties": { - "resource_id": { - "$ref": "#/components/schemas/PaymentIdType" - }, - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", - "nullable": true - }, - "force_sync": { - "type": "boolean", - "description": "Decider to enable or disable the connector call for retrieve request" - }, - "param": { - "type": "string", - "description": "The parameters passed to a retrieve request", - "nullable": true - }, - "connector": { - "type": "string", - "description": "The name of the connector", - "nullable": true - }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], - "nullable": true - }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK", - "nullable": true - }, - "expand_captures": { - "type": "boolean", - "description": "If enabled provides list of captures linked to latest attempt", - "nullable": true - }, - "expand_attempts": { - "type": "boolean", - "description": "If enabled provides list of attempts linked to payment intent", - "nullable": true - } - } - }, - "PaymentsSessionRequest": { - "type": "object", - "required": [ - "payment_id", - "client_secret", - "wallets" - ], - "properties": { - "payment_id": { - "type": "string", - "description": "The identifier for the payment" - }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" - }, - "wallets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "The list of the supported wallets" - }, - "merchant_connector_details": { + "payment_type": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], - "nullable": true - } - } - }, - "PaymentsSessionResponse": { - "type": "object", - "required": [ - "payment_id", - "client_secret", - "session_token" - ], - "properties": { - "payment_id": { - "type": "string", - "description": "The identifier for the payment" - }, - "client_secret": { - "type": "string", - "description": "This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK" - }, - "session_token": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SessionToken" - }, - "description": "The list of session token object" - } - } - }, - "PaymentsStartRequest": { - "type": "object", - "required": [ - "payment_id", - "merchant_id", - "attempt_id" - ], - "properties": { - "payment_id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant. This field is auto generated and is returned in the API response." + "$ref": "#/components/schemas/PaymentType" + } + ], + "nullable": true }, - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account." + "request_incremental_authorization": { + "type": "boolean", + "description": "Request for an incremental authorization", + "nullable": true }, - "attempt_id": { - "type": "string", - "description": "The identifier for the payment transaction" + "session_expiry": { + "type": "integer", + "format": "int32", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, @@ -11633,722 +14504,1056 @@ } ] }, - "PayoutRetrieveBody": { + "PayoutRetrieveBody": { + "type": "object", + "properties": { + "force_sync": { + "type": "boolean", + "nullable": true + } + } + }, + "PayoutRetrieveRequest": { + "type": "object", + "required": [ + "payout_id" + ], + "properties": { + "payout_id": { + "type": "string", + "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "payout_mbabizu24mvu3mela5njyhpit4", + "maxLength": 30, + "minLength": 30 + }, + "force_sync": { + "type": "boolean", + "description": "`force_sync` with the connector to get payout details\n(defaults to false)", + "default": false, + "example": true, + "nullable": true + } + } + }, + "PayoutStatus": { + "type": "string", + "enum": [ + "success", + "failed", + "cancelled", + "pending", + "ineligible", + "requires_creation", + "requires_payout_method_data", + "requires_fulfillment" + ] + }, + "PayoutType": { + "type": "string", + "enum": [ + "card", + "bank" + ] + }, + "PaypalRedirection": { + "type": "object" + }, + "PaypalSessionTokenResponse": { + "type": "object", + "required": [ + "session_token" + ], + "properties": { + "session_token": { + "type": "string", + "description": "The session token for PayPal" + } + } + }, + "PhoneDetails": { + "type": "object", + "properties": { + "number": { + "type": "string", + "description": "The contact number", + "example": "9999999999", + "nullable": true + }, + "country_code": { + "type": "string", + "description": "The country code attached to the number", + "example": "+1", + "nullable": true + } + } + }, + "PrimaryBusinessDetails": { + "type": "object", + "required": [ + "country", + "business" + ], + "properties": { + "country": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "business": { + "type": "string", + "example": "food" + } + } + }, + "ProductType": { + "type": "string", + "enum": [ + "physical", + "digital", + "travel", + "ride", + "event", + "accommodation" + ] + }, + "ProfileDefaultRoutingConfig": { + "type": "object", + "required": [ + "profile_id", + "connectors" + ], + "properties": { + "profile_id": { + "type": "string" + }, + "connectors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + "ProgramConnectorSelection": { + "type": "object", + "description": "The program, having a default connector selection and\na bunch of rules. Also can hold arbitrary metadata.", + "required": [ + "defaultSelection", + "rules", + "metadata" + ], + "properties": { + "defaultSelection": { + "$ref": "#/components/schemas/ConnectorSelection" + }, + "rules": { + "$ref": "#/components/schemas/RuleConnectorSelection" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + } + }, + "ReceiverDetails": { + "type": "object", + "required": [ + "amount_received" + ], + "properties": { + "amount_received": { + "type": "integer", + "format": "int64", + "description": "The amount received by receiver" + }, + "amount_charged": { + "type": "integer", + "format": "int64", + "description": "The amount charged by ACH", + "nullable": true + }, + "amount_remaining": { + "type": "integer", + "format": "int64", + "description": "The amount remaining to be sent via ACH", + "nullable": true + } + } + }, + "ReconStatus": { + "type": "string", + "enum": [ + "not_requested", + "requested", + "active", + "disabled" + ] + }, + "RedirectResponse": { + "type": "object", + "properties": { + "param": { + "type": "string", + "nullable": true + }, + "json_payload": { + "type": "object", + "nullable": true + } + } + }, + "RefundListRequest": { + "allOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/TimeRange" + } + ], + "nullable": true + }, + { + "type": "object", + "properties": { + "payment_id": { + "type": "string", + "description": "The identifier for the payment", + "nullable": true + }, + "refund_id": { + "type": "string", + "description": "The identifier for the refund", + "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The identifier for business profile", + "nullable": true + }, + "limit": { + "type": "integer", + "format": "int64", + "description": "Limit on the number of objects to return", + "nullable": true + }, + "offset": { + "type": "integer", + "format": "int64", + "description": "The starting point within a list of objects", + "nullable": true + }, + "connector": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of connectors to filter refunds list", + "nullable": true + }, + "currency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + }, + "description": "The list of currencies to filter refunds list", + "nullable": true + }, + "refund_status": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefundStatus" + }, + "description": "The list of refund statuses to filter refunds list", + "nullable": true + } + } + } + ] + }, + "RefundListResponse": { "type": "object", + "required": [ + "count", + "total_count", + "data" + ], "properties": { - "force_sync": { - "type": "boolean", - "nullable": true + "count": { + "type": "integer", + "description": "The number of refunds included in the list", + "minimum": 0 + }, + "total_count": { + "type": "integer", + "format": "int64", + "description": "The total number of refunds in the list" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RefundResponse" + }, + "description": "The List of refund response object" } } }, - "PayoutRetrieveRequest": { + "RefundRequest": { "type": "object", "required": [ - "payout_id" + "payment_id" ], "properties": { - "payout_id": { + "payment_id": { "type": "string", - "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", - "example": "payout_mbabizu24mvu3mela5njyhpit4", + "description": "The payment id against which refund is to be intiated", + "example": "pay_mbabizu24mvu3mela5njyhpit4", "maxLength": 30, "minLength": 30 }, - "force_sync": { - "type": "boolean", - "description": "`force_sync` with the connector to get payout details\n(defaults to false)", - "default": false, - "example": true, - "nullable": true - } - } - }, - "PayoutStatus": { - "type": "string", - "enum": [ - "success", - "failed", - "cancelled", - "pending", - "ineligible", - "requires_creation", - "requires_payout_method_data", - "requires_fulfillment" - ] - }, - "PayoutType": { - "type": "string", - "enum": [ - "card", - "bank" - ] - }, - "PaypalRedirection": { - "type": "object" - }, - "PaypalSessionTokenResponse": { - "type": "object", - "required": [ - "session_token" - ], - "properties": { - "session_token": { + "refund_id": { "type": "string", - "description": "The session token for PayPal" - } - } - }, - "PhoneDetails": { - "type": "object", - "properties": { - "number": { + "description": "Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refunds initiated against the same payment. If this is not passed by the merchant, this field shall be auto generated and provided in the API response. It is recommended to generate uuid(v4) as the refund_id.", + "example": "ref_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "merchant_id": { "type": "string", - "description": "The contact number", - "example": "9999999999", - "nullable": true + "description": "The identifier for the Merchant Account", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 255 }, - "country_code": { + "amount": { + "type": "integer", + "format": "int64", + "description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, this will default to the full payment amount", + "example": 6540, + "nullable": true, + "minimum": 100 + }, + "reason": { "type": "string", - "description": "The country code attached to the number", - "example": "+1", + "description": "Reason for the refund. Often useful for displaying to users and your customer support executive. In case the payment went through Stripe, this field needs to be passed with one of these enums: `duplicate`, `fraudulent`, or `requested_by_customer`", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 + }, + "refund_type": { + "allOf": [ + { + "$ref": "#/components/schemas/RefundType" + } + ], + "default": "Instant", + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "nullable": true + }, + "merchant_connector_details": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + } + ], "nullable": true } } }, - "PrimaryBusinessDetails": { + "RefundResponse": { "type": "object", "required": [ - "country", - "business" + "refund_id", + "payment_id", + "amount", + "currency", + "status", + "connector" ], "properties": { - "country": { - "$ref": "#/components/schemas/CountryAlpha2" + "refund_id": { + "type": "string", + "description": "Unique Identifier for the refund" }, - "business": { + "payment_id": { "type": "string", - "example": "food" - } - } - }, - "ProductType": { - "type": "string", - "enum": [ - "physical", - "digital", - "travel", - "ride", - "event", - "accommodation" - ] - }, - "ReceiverDetails": { - "type": "object", - "required": [ - "amount_received" - ], - "properties": { - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount received by receiver" + "description": "The payment id against which refund is intiated" }, - "amount_charged": { + "amount": { "type": "integer", "format": "int64", - "description": "The amount charged by ACH", + "description": "The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc" + }, + "currency": { + "type": "string", + "description": "The three-letter ISO currency code" + }, + "status": { + "$ref": "#/components/schemas/RefundStatus" + }, + "reason": { + "type": "string", + "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", "nullable": true }, - "amount_remaining": { - "type": "integer", - "format": "int64", - "description": "The amount remaining to be sent via ACH", + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object", + "nullable": true + }, + "error_message": { + "type": "string", + "description": "The error message", + "nullable": true + }, + "error_code": { + "type": "string", + "description": "The code for the error", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The timestamp at which refund is created", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "The timestamp at which refund is updated", + "nullable": true + }, + "connector": { + "type": "string", + "description": "The connector used for the refund and the corresponding payment", + "example": "stripe" + }, + "profile_id": { + "type": "string", + "description": "The id of business profile for this refund", + "nullable": true + }, + "merchant_connector_id": { + "type": "string", + "description": "The merchant_connector_id of the processor through which this payment went through", "nullable": true } } }, - "ReconStatus": { + "RefundStatus": { + "type": "string", + "description": "The status for refunds", + "enum": [ + "succeeded", + "failed", + "pending", + "review" + ] + }, + "RefundType": { "type": "string", + "description": "To indicate whether to refund needs to be instant or scheduled", "enum": [ - "not_requested", - "requested", - "active", - "disabled" + "scheduled", + "instant" ] }, - "RedirectResponse": { + "RefundUpdateRequest": { "type": "object", "properties": { - "param": { + "reason": { "type": "string", - "nullable": true + "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 }, - "json_payload": { + "metadata": { "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", "nullable": true } } }, - "RefundListRequest": { - "allOf": [ - { + "RequestPaymentMethodTypes": { + "type": "object", + "required": [ + "payment_method_type", + "recurring_enabled", + "installment_payment_enabled" + ], + "properties": { + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_experience": { "allOf": [ { - "$ref": "#/components/schemas/TimeRange" + "$ref": "#/components/schemas/PaymentExperience" } ], "nullable": true }, - { - "type": "object", - "properties": { - "payment_id": { - "type": "string", - "description": "The identifier for the payment", - "nullable": true - }, - "refund_id": { - "type": "string", - "description": "The identifier for the refund", - "nullable": true - }, - "profile_id": { - "type": "string", - "description": "The identifier for business profile", - "nullable": true - }, - "limit": { - "type": "integer", - "format": "int64", - "description": "Limit on the number of objects to return", - "nullable": true - }, - "offset": { - "type": "integer", - "format": "int64", - "description": "The starting point within a list of objects", - "nullable": true - }, - "connector": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of connectors to filter refunds list", - "nullable": true - }, - "currency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" - }, - "description": "The list of currencies to filter refunds list", - "nullable": true - }, - "refund_status": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundStatus" - }, - "description": "The list of refund statuses to filter refunds list", - "nullable": true + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetwork" + }, + "nullable": true + }, + "accepted_currencies": { + "allOf": [ + { + "$ref": "#/components/schemas/AcceptedCurrencies" } - } + ], + "nullable": true + }, + "accepted_countries": { + "allOf": [ + { + "$ref": "#/components/schemas/AcceptedCountries" + } + ], + "nullable": true + }, + "minimum_amount": { + "type": "integer", + "format": "int32", + "description": "Minimum amount supported by the processor. To be represented in the lowest denomination of the target currency (For example, for USD it should be in cents)", + "example": 1, + "nullable": true + }, + "maximum_amount": { + "type": "integer", + "format": "int32", + "description": "Maximum amount supported by the processor. To be represented in the lowest denomination of\nthe target currency (For example, for USD it should be in cents)", + "example": 1313, + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Boolean to enable recurring payments / mandates. Default is true.", + "default": true, + "example": false + }, + "installment_payment_enabled": { + "type": "boolean", + "description": "Boolean to enable installment / EMI / BNPL payments. Default is true.", + "default": true, + "example": false } - ] + } }, - "RefundListResponse": { + "RequestSurchargeDetails": { "type": "object", "required": [ - "count", - "total_count", - "data" + "surcharge_amount" ], "properties": { - "count": { + "surcharge_amount": { "type": "integer", - "description": "The number of refunds included in the list", - "minimum": 0 + "format": "int64" }, - "total_count": { + "tax_amount": { "type": "integer", "format": "int64", - "description": "The total number of refunds in the list" + "nullable": true + } + } + }, + "RequiredFieldInfo": { + "type": "object", + "description": "Required fields info used while listing the payment_method_data", + "required": [ + "required_field", + "display_name", + "field_type" + ], + "properties": { + "required_field": { + "type": "string", + "description": "Required field for a payment_method through a payment_method_type" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "The List of refund response object" + "display_name": { + "type": "string", + "description": "Display name of the required field in the front-end" + }, + "field_type": { + "$ref": "#/components/schemas/FieldType" + }, + "value": { + "type": "string", + "nullable": true } } }, - "RefundRequest": { + "RetrieveApiKeyResponse": { "type": "object", + "description": "The response body for retrieving an API Key.", "required": [ - "payment_id" + "key_id", + "merchant_id", + "name", + "prefix", + "created", + "expiration" ], "properties": { - "refund_id": { + "key_id": { "type": "string", - "description": "Unique Identifier for the Refund. This is to ensure idempotency for multiple partial refund initiated against the same payment. If the identifiers is not defined by the merchant, this filed shall be auto generated and provide in the API response. It is recommended to generate uuid(v4) as the refund_id.", - "example": "ref_mbabizu24mvu3mela5njyhpit4", + "description": "The identifier for the API Key.", + "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", + "maxLength": 64 + }, + "merchant_id": { + "type": "string", + "description": "The identifier for the Merchant Account.", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 + }, + "name": { + "type": "string", + "description": "The unique name for the API Key to help you identify it.", + "example": "Sandbox integration key", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "The description to provide more context about the API Key.", + "example": "Key used by our developers to integrate with the sandbox environment", "nullable": true, - "maxLength": 30, - "minLength": 30 + "maxLength": 256 }, - "payment_id": { + "prefix": { "type": "string", - "description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc. If not provided, this will default to the full payment amount", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 + "description": "The first few characters of the plaintext API Key to help you identify it.", + "maxLength": 64 + }, + "created": { + "type": "string", + "format": "date-time", + "description": "The time at which the API Key was created.", + "example": "2022-09-10T10:11:12Z" + }, + "expiration": { + "$ref": "#/components/schemas/ApiKeyExpiration" + } + } + }, + "RetrievePaymentLinkRequest": { + "type": "object", + "properties": { + "client_secret": { + "type": "string", + "nullable": true + } + } + }, + "RetrievePaymentLinkResponse": { + "type": "object", + "required": [ + "payment_link_id", + "merchant_id", + "link_to_pay", + "amount", + "created_at", + "status" + ], + "properties": { + "payment_link_id": { + "type": "string" }, "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 255 + "type": "string" + }, + "link_to_pay": { + "type": "string" }, "amount": { "type": "integer", - "format": "int64", - "description": "Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, this will default to the full payment amount", - "example": 6540, - "nullable": true, - "minimum": 100 + "format": "int64" }, - "reason": { + "created_at": { "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", - "example": "Customer returned the product", - "nullable": true, - "maxLength": 255 + "format": "date-time" }, - "refund_type": { - "allOf": [ - { - "$ref": "#/components/schemas/RefundType" - } - ], - "default": "Instant", + "expiry": { + "type": "string", + "format": "date-time", "nullable": true }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": { + "type": "string", "nullable": true }, - "merchant_connector_details": { + "status": { + "$ref": "#/components/schemas/PaymentLinkStatus" + }, + "currency": { "allOf": [ { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" + "$ref": "#/components/schemas/Currency" } ], "nullable": true } } }, - "RefundResponse": { + "RetryAction": { + "type": "string", + "enum": [ + "manual_retry", + "requeue" + ] + }, + "RevokeApiKeyResponse": { "type": "object", + "description": "The response body for revoking an API Key.", "required": [ - "refund_id", - "payment_id", - "amount", - "currency", - "status", - "connector" + "merchant_id", + "key_id", + "revoked" ], "properties": { - "refund_id": { + "merchant_id": { "type": "string", - "description": "The identifier for refund" + "description": "The identifier for the Merchant Account.", + "example": "y3oqhf46pyzuxjbcn2giaqnb44", + "maxLength": 64 }, - "payment_id": { + "key_id": { "type": "string", - "description": "The identifier for payment" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc" + "description": "The identifier for the API Key.", + "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", + "maxLength": 64 }, - "currency": { + "revoked": { + "type": "boolean", + "description": "Indicates whether the API key was revoked or not.", + "example": "true" + } + } + }, + "RewardData": { + "type": "object", + "required": [ + "merchant_id" + ], + "properties": { + "merchant_id": { "type": "string", - "description": "The three-letter ISO currency code" + "description": "The merchant ID with which we have to call the connector" + } + } + }, + "RoutableChoiceKind": { + "type": "string", + "enum": [ + "OnlyConnector", + "FullStruct" + ] + }, + "RoutableConnectorChoice": { + "type": "object", + "description": "Routable Connector chosen for a payment", + "required": [ + "connector" + ], + "properties": { + "connector": { + "$ref": "#/components/schemas/RoutableConnectors" }, - "reason": { + "sub_label": { "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", "nullable": true - }, - "status": { - "$ref": "#/components/schemas/RefundStatus" - }, - "metadata": { + } + } + }, + "RoutableConnectors": { + "type": "string", + "description": "Connectors eligible for payments routing", + "enum": [ + "aci", + "adyen", + "airwallex", + "authorizedotnet", + "bankofamerica", + "bitpay", + "bambora", + "bluesnap", + "boku", + "braintree", + "cashtocode", + "checkout", + "coinbase", + "cryptopay", + "cybersource", + "dlocal", + "fiserv", + "forte", + "globalpay", + "globepay", + "gocardless", + "helcim", + "iatapay", + "klarna", + "mollie", + "multisafepay", + "nexinets", + "nmi", + "noon", + "nuvei", + "opennode", + "payme", + "paypal", + "payu", + "placetopay", + "powertranz", + "prophetpay", + "rapyd", + "riskified", + "shift4", + "signifyd", + "square", + "stax", + "stripe", + "trustpay", + "tsys", + "volt", + "wise", + "worldline", + "worldpay", + "zen" + ] + }, + "RoutingAlgorithm": { + "oneOf": [ + { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object", - "nullable": true - }, - "error_message": { - "type": "string", - "description": "The error message", - "nullable": true - }, - "error_code": { - "type": "string", - "description": "The code for the error", - "nullable": true - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "The timestamp at which refund is created", - "nullable": true - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "The timestamp at which refund is updated", - "nullable": true + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "single" + ] + }, + "data": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } }, - "connector": { - "type": "string", - "description": "The connector used for the refund and the corresponding payment", - "example": "stripe" + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } }, - "profile_id": { - "type": "string", - "description": "The id of business profile for this refund", - "nullable": true + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "volume_split" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorVolumeSplit" + } + } + } }, - "merchant_connector_id": { - "type": "string", - "description": "The merchant_connector_id of the processor through which this payment went through", - "nullable": true + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "advanced" + ] + }, + "data": { + "$ref": "#/components/schemas/ProgramConnectorSelection" + } + } } + ], + "description": "Routing Algorithm kind", + "discriminator": { + "propertyName": "type" } }, - "RefundStatus": { - "type": "string", - "description": "The status for refunds", - "enum": [ - "succeeded", - "failed", - "pending", - "review" - ] - }, - "RefundType": { + "RoutingAlgorithmKind": { "type": "string", "enum": [ - "scheduled", - "instant" + "single", + "priority", + "volume_split", + "advanced" ] }, - "RefundUpdateRequest": { + "RoutingConfigRequest": { "type": "object", "properties": { - "reason": { + "name": { "type": "string", - "description": "An arbitrary string attached to the object. Often useful for displaying to users and your customer support executive", - "example": "Customer returned the product", - "nullable": true, - "maxLength": 255 - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - } - } - }, - "RequestPaymentMethodTypes": { - "type": "object", - "required": [ - "payment_method_type", - "recurring_enabled", - "installment_payment_enabled" - ], - "properties": { - "payment_method_type": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "payment_experience": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentExperience" - } - ], - "nullable": true - }, - "card_networks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CardNetwork" - }, "nullable": true }, - "accepted_currencies": { - "allOf": [ - { - "$ref": "#/components/schemas/AcceptedCurrencies" - } - ], + "description": { + "type": "string", "nullable": true }, - "accepted_countries": { + "algorithm": { "allOf": [ { - "$ref": "#/components/schemas/AcceptedCountries" + "$ref": "#/components/schemas/RoutingAlgorithm" } ], "nullable": true }, - "minimum_amount": { - "type": "integer", - "format": "int32", - "description": "Minimum amount supported by the processor. To be represented in the lowest denomination of the target currency (For example, for USD it should be in cents)", - "example": 1, - "nullable": true - }, - "maximum_amount": { - "type": "integer", - "format": "int32", - "description": "Maximum amount supported by the processor. To be represented in the lowest denomination of\nthe target currency (For example, for USD it should be in cents)", - "example": 1313, - "nullable": true - }, - "recurring_enabled": { - "type": "boolean", - "description": "Boolean to enable recurring payments / mandates. Default is true.", - "default": true, - "example": false - }, - "installment_payment_enabled": { - "type": "boolean", - "description": "Boolean to enable installment / EMI / BNPL payments. Default is true.", - "default": true, - "example": false - } - } - }, - "RequestSurchargeDetails": { - "type": "object", - "required": [ - "surcharge_amount" - ], - "properties": { - "surcharge_amount": { - "type": "integer", - "format": "int64" - }, - "tax_amount": { - "type": "integer", - "format": "int64", - "nullable": true - } - } - }, - "RequiredFieldInfo": { - "type": "object", - "description": "Required fields info used while listing the payment_method_data", - "required": [ - "required_field", - "display_name", - "field_type" - ], - "properties": { - "required_field": { - "type": "string", - "description": "Required field for a payment_method through a payment_method_type" - }, - "display_name": { - "type": "string", - "description": "Display name of the required field in the front-end" - }, - "field_type": { - "$ref": "#/components/schemas/FieldType" - }, - "value": { + "profile_id": { "type": "string", "nullable": true } } }, - "RetrieveApiKeyResponse": { + "RoutingDictionary": { "type": "object", - "description": "The response body for retrieving an API Key.", "required": [ - "key_id", "merchant_id", - "name", - "prefix", - "created", - "expiration" + "records" ], "properties": { - "key_id": { - "type": "string", - "description": "The identifier for the API Key.", - "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", - "maxLength": 64 - }, "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64 - }, - "name": { - "type": "string", - "description": "The unique name for the API Key to help you identify it.", - "example": "Sandbox integration key", - "maxLength": 64 - }, - "description": { - "type": "string", - "description": "The description to provide more context about the API Key.", - "example": "Key used by our developers to integrate with the sandbox environment", - "nullable": true, - "maxLength": 256 - }, - "prefix": { - "type": "string", - "description": "The first few characters of the plaintext API Key to help you identify it.", - "maxLength": 64 - }, - "created": { - "type": "string", - "format": "date-time", - "description": "The time at which the API Key was created.", - "example": "2022-09-10T10:11:12Z" + "type": "string" }, - "expiration": { - "$ref": "#/components/schemas/ApiKeyExpiration" - } - } - }, - "RetrievePaymentLinkRequest": { - "type": "object", - "properties": { - "client_secret": { + "active_id": { "type": "string", "nullable": true + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } } } }, - "RetrievePaymentLinkResponse": { + "RoutingDictionaryRecord": { "type": "object", "required": [ - "payment_link_id", - "merchant_id", - "link_to_pay", - "amount", + "id", + "name", + "kind", + "description", "created_at", - "status" + "modified_at" ], "properties": { - "payment_link_id": { + "id": { "type": "string" }, - "merchant_id": { + "name": { "type": "string" }, - "link_to_pay": { + "kind": { + "$ref": "#/components/schemas/RoutingAlgorithmKind" + }, + "description": { "type": "string" }, - "amount": { + "created_at": { "type": "integer", "format": "int64" }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "expiry": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/PaymentLinkStatus" + "modified_at": { + "type": "integer", + "format": "int64" + } + } + }, + "RoutingKind": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoutingDictionary" }, - "currency": { + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + ] + }, + "RoutingRetrieveResponse": { + "type": "object", + "description": "Response of the retrieved routing configs for a merchant account", + "properties": { + "algorithm": { "allOf": [ { - "$ref": "#/components/schemas/Currency" + "$ref": "#/components/schemas/MerchantRoutingAlgorithm" } ], "nullable": true } } }, - "RetryAction": { - "type": "string", - "enum": [ - "manual_retry", - "requeue" - ] - }, - "RevokeApiKeyResponse": { + "RuleConnectorSelection": { "type": "object", - "description": "The response body for revoking an API Key.", + "description": "Represents a rule\n\n```text\nrule_name: [stripe, adyen, checkout]\n{\npayment.method = card {\npayment.method.cardtype = (credit, debit) {\npayment.method.network = (amex, rupay, diners)\n}\n\npayment.method.cardtype = credit\n}\n}\n```", "required": [ - "merchant_id", - "key_id", - "revoked" + "name", + "connectorSelection", + "statements" ], "properties": { - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account.", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64 + "name": { + "type": "string" }, - "key_id": { - "type": "string", - "description": "The identifier for the API Key.", - "example": "5hEEqkgJUyuxgSKGArHA4mWSnX", - "maxLength": 64 + "connectorSelection": { + "$ref": "#/components/schemas/ConnectorSelection" }, - "revoked": { - "type": "boolean", - "description": "Indicates whether the API key was revoked or not.", - "example": "true" - } - } - }, - "RewardData": { - "type": "object", - "required": [ - "merchant_id" - ], - "properties": { - "merchant_id": { - "type": "string", - "description": "The merchant ID with which we have to call the connector" + "statements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IfStatement" + } } } }, - "RoutingAlgorithm": { - "type": "string", - "description": "The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'", - "enum": [ - "round_robin", - "max_conversion", - "min_cost", - "custom" - ], - "example": "custom" - }, "SamsungPayWalletData": { "type": "object", "required": [ @@ -12607,6 +15812,76 @@ } } }, + "StraightThroughAlgorithm": { + "oneOf": [ + { + "type": "object", + "title": "Single", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "single" + ] + }, + "data": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + }, + { + "type": "object", + "title": "Priority", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "priority" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutableConnectorChoice" + } + } + } + }, + { + "type": "object", + "title": "VolumeSplit", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "volume_split" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorVolumeSplit" + } + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "SurchargeDetailsResponse": { "type": "object", "required": [ @@ -12781,6 +16056,157 @@ } } }, + "ValueType": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "number" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Represents a number literal" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "enum_variant" + ] + }, + "value": { + "type": "string", + "description": "Represents an enum variant" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "metadata_variant" + ] + }, + "value": { + "$ref": "#/components/schemas/MetadataValue" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "str_value" + ] + }, + "value": { + "type": "string", + "description": "Represents a arbitrary String value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "number_array" + ] + }, + "value": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "description": "Represents an array of numbers. This is basically used for\n\"one of the given numbers\" operations\neg: payment.method.amount = (1, 2, 3)" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "enum_variant_array" + ] + }, + "value": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Similar to NumberArray but for enum variants\neg: payment.method.cardtype = (debit, credit)" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "number_comparison_array" + ] + }, + "value": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NumberComparison" + }, + "description": "Like a number array but can include comparisons. Useful for\nconditions like \"500 < amount < 1000\"\neg: payment.amount = (> 500, < 1000)" + } + } + } + ], + "description": "Represents a value in the DSL", + "discriminator": { + "propertyName": "type" + } + }, "VoucherData": { "oneOf": [ { @@ -13278,7 +16704,7 @@ "type": "apiKey", "in": "header", "name": "api-key", - "description": "API keys are the most common method of authentication and can be obtained from the HyperSwitch dashboard." + "description": "Use the API key created under your merchant account from the HyperSwitch dashboard. API key is used to authenticate API requests from your merchant server only. Don't expose this key on a website or embed it in a mobile application." }, "ephemeral_key": { "type": "apiKey", @@ -13327,6 +16753,10 @@ "name": "Disputes", "description": "Manage disputes" }, + { + "name": "API Key", + "description": "Create and manage API Keys" + }, { "name": "Payouts", "description": "Create and manage payouts" @@ -13334,6 +16764,10 @@ { "name": "payment link", "description": "Create payment link" + }, + { + "name": "Routing", + "description": "Create and manage routing configurations" } ] } \ No newline at end of file From 3fbffdc242dafe7983c542573b7c6362f99331e6 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:29:07 +0530 Subject: [PATCH 400/443] chore(configs): [NMI] add wasm changes for prod dashboard (#3470) --- crates/connector_configs/toml/production.toml | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 7d4931f72d1e..04e86b5a8792 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1077,8 +1077,42 @@ merchant_capabilities=["supports3DS"] label="apple" [nmi] -[[nmi.bank_redirect]] - payment_method_type = "ideal" +[[nmi.credit]] + payment_method_type = "Mastercard" +[[nmi.credit]] + payment_method_type = "Visa" +[[nmi.credit]] + payment_method_type = "Interac" +[[nmi.credit]] + payment_method_type = "AmericanExpress" +[[nmi.credit]] + payment_method_type = "JCB" +[[nmi.credit]] + payment_method_type = "DinersClub" +[[nmi.credit]] + payment_method_type = "Discover" +[[nmi.credit]] + payment_method_type = "CartesBancaires" +[[nmi.credit]] + payment_method_type = "UnionPay" +[[nmi.debit]] + payment_method_type = "Mastercard" +[[nmi.debit]] + payment_method_type = "Visa" +[[nmi.debit]] + payment_method_type = "Interac" +[[nmi.debit]] + payment_method_type = "AmericanExpress" +[[nmi.debit]] + payment_method_type = "JCB" +[[nmi.debit]] + payment_method_type = "DinersClub" +[[nmi.debit]] + payment_method_type = "Discover" +[[nmi.debit]] + payment_method_type = "CartesBancaires" +[[nmi.debit]] + payment_method_type = "UnionPay" [nmi.connector_auth.BodyKey] api_key="API Key" key1="Public Key" From c9d41e2169decf3e9a26999c37fe81b6a8c0362f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:18:24 +0000 Subject: [PATCH 401/443] chore(version): 2024.01.30.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d7a952adc2..c2053240e80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.30.0 + +### Features + +- **router:** Add request_details logger middleware for 400 bad requests ([#3414](https://github.com/juspay/hyperswitch/pull/3414)) ([`dd0d2dc`](https://github.com/juspay/hyperswitch/commit/dd0d2dc2dd9a6263bbb8a99d1f0b2077f38dd621)) + +### Refactors + +- **openapi:** Move openapi to separate crate to decrease compile times ([#3110](https://github.com/juspay/hyperswitch/pull/3110)) ([`7d8d68f`](https://github.com/juspay/hyperswitch/commit/7d8d68faba55dfcb2886c63ae7969ebd4b9ec98c)) + +### Miscellaneous Tasks + +- **configs:** [NMI] add wasm changes for prod dashboard ([#3470](https://github.com/juspay/hyperswitch/pull/3470)) ([`3fbffdc`](https://github.com/juspay/hyperswitch/commit/3fbffdc242dafe7983c542573b7c6362f99331e6)) + +**Full Changelog:** [`2024.01.29.0...2024.01.30.0`](https://github.com/juspay/hyperswitch/compare/2024.01.29.0...2024.01.30.0) + +- - - + ## 2024.01.29.0 ### Features From 02074dfc23f1a126e76935ba5311c6aed6590ca5 Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:35:42 +0530 Subject: [PATCH 402/443] feat(core): update card_details for an existing mandate (#3452) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 12 ++- crates/data_models/src/mandates.rs | 17 +++ .../src/payments/payment_attempt.rs | 8 +- crates/diesel_models/src/enums.rs | 46 ++++++++ crates/diesel_models/src/mandate.rs | 31 +++++- crates/diesel_models/src/payment_attempt.rs | 4 +- crates/diesel_models/src/user/sample_data.rs | 4 +- .../stripe/payment_intents/types.rs | 1 + crates/router/src/core/mandate.rs | 102 +++++++++++++----- crates/router/src/core/mandate/helpers.rs | 35 ++++++ .../router/src/core/payment_methods/cards.rs | 17 ++- .../core/payments/flows/setup_mandate_flow.rs | 93 ++++++++++++++-- crates/router/src/core/payments/helpers.rs | 2 +- .../payments/operations/payment_confirm.rs | 24 ++++- .../payments/operations/payment_create.rs | 50 +++++++-- .../payments/operations/payment_update.rs | 5 +- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/db/mandate.rs | 12 +++ crates/router/src/services/api.rs | 2 +- crates/router/src/types/transformers.rs | 1 + .../src/payments/payment_attempt.rs | 51 ++++++++- openapi/openapi_spec.json | 5 + 22 files changed, 451 insertions(+), 72 deletions(-) create mode 100644 crates/router/src/core/mandate/helpers.rs diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 57c66dc5a7b6..8c27f498d7ad 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -617,10 +617,18 @@ pub enum MandateReferenceId { NetworkMandateId(String), // network_txns_id send by Issuer to connector, Used for PG agnostic mandate txns } -#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] pub struct ConnectorMandateReferenceId { pub connector_mandate_id: Option, pub payment_method_id: Option, + pub update_history: Option>, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct UpdateHistory { + pub connector_mandate_id: Option, + pub payment_method_id: String, + pub original_payment_id: Option, } impl MandateIds { @@ -637,6 +645,8 @@ impl MandateIds { #[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct MandateData { + /// A way to update the mandate's payment method details + pub update_mandate_id: Option, /// A concent from the customer to store the payment method pub customer_acceptance: Option, /// A way to select the type of mandate used diff --git a/crates/data_models/src/mandates.rs b/crates/data_models/src/mandates.rs index afdcda3a40e7..319a78cf661f 100644 --- a/crates/data_models/src/mandates.rs +++ b/crates/data_models/src/mandates.rs @@ -9,6 +9,13 @@ use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct MandateDetails { + pub update_mandate_id: Option, + pub mandate_type: Option, +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MandateDataType { @@ -16,6 +23,13 @@ pub enum MandateDataType { MultiUse(Option), } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum MandateTypeDetails { + MandateType(MandateDataType), + MandateDetails(MandateDetails), +} #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MandateAmountData { pub amount: i64, @@ -29,6 +43,8 @@ pub struct MandateAmountData { // information about creating mandates #[derive(Default, Eq, PartialEq, Debug, Clone)] pub struct MandateData { + /// A way to update the mandate's payment method details + pub update_mandate_id: Option, /// A concent from the customer to store the payment method pub customer_acceptance: Option, /// A way to select the type of mandate used @@ -90,6 +106,7 @@ impl From for MandateData { Self { customer_acceptance: value.customer_acceptance.map(|d| d.into()), mandate_type: value.mandate_type.map(|d| d.into()), + update_mandate_id: value.update_mandate_id, } } } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 3e6ba9e37f8a..e6b9950b4595 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use super::PaymentIntent; -use crate::{errors, mandates::MandateDataType, ForeignIDRef}; +use crate::{errors, mandates::MandateTypeDetails, ForeignIDRef}; #[async_trait::async_trait] pub trait PaymentAttemptInterface { @@ -143,7 +143,7 @@ pub struct PaymentAttempt { pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub multiple_capture_count: Option, // reference to the payment at connector side @@ -184,7 +184,7 @@ pub struct PaymentAttemptNew { pub attempt_id: String, pub status: storage_enums::AttemptStatus, pub amount: i64, - /// amount + surcharge_amount + tax_amount + /// amount + surcharge_amount + tax_amount /// This field will always be derived before updating in the Database pub net_amount: i64, pub currency: Option, @@ -221,7 +221,7 @@ pub struct PaymentAttemptNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub multiple_capture_count: Option, diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index a06937c99a6d..babffdbc4a86 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -166,6 +166,17 @@ use diesel::{ expression::AsExpression, sql_types::Jsonb, }; + +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, +)] +#[diesel(sql_type = Jsonb)] +#[serde(rename_all = "snake_case")] +pub struct MandateDetails { + pub update_mandate_id: Option, + pub mandate_type: Option, +} + #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, )] @@ -185,6 +196,7 @@ where Ok(serde_json::from_value(value)?) } } + impl ToSql for MandateDataType where serde_json::Value: ToSql, @@ -199,6 +211,40 @@ where } } +#[derive( + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, +)] +#[diesel(sql_type = Jsonb)] +#[serde(untagged)] +#[serde(rename_all = "snake_case")] +pub enum MandateTypeDetails { + MandateType(MandateDataType), + MandateDetails(MandateDetails), +} + +impl FromSql for MandateTypeDetails +where + serde_json::Value: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value(value)?) + } +} +impl ToSql for MandateTypeDetails +where + serde_json::Value: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + + // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends + // please refer to the diesel migration blog: + // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations + >::to_sql(&value, &mut out.reborrow()) + } +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MandateAmountData { pub amount: i64, diff --git a/crates/diesel_models/src/mandate.rs b/crates/diesel_models/src/mandate.rs index cc3474914c01..31c6ef62f2b4 100644 --- a/crates/diesel_models/src/mandate.rs +++ b/crates/diesel_models/src/mandate.rs @@ -75,6 +75,12 @@ pub enum MandateUpdate { ConnectorReferenceUpdate { connector_mandate_ids: Option, }, + ConnectorMandateIdUpdate { + connector_mandate_id: Option, + connector_mandate_ids: Option, + payment_method_id: String, + original_payment_id: Option, + }, } #[derive(Clone, Eq, PartialEq, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] @@ -89,6 +95,9 @@ pub struct MandateUpdateInternal { mandate_status: Option, amount_captured: Option, connector_mandate_ids: Option, + connector_mandate_id: Option, + payment_method_id: Option, + original_payment_id: Option, } impl From for MandateUpdateInternal { @@ -98,16 +107,34 @@ impl From for MandateUpdateInternal { mandate_status: Some(mandate_status), connector_mandate_ids: None, amount_captured: None, + connector_mandate_id: None, + payment_method_id: None, + original_payment_id: None, }, MandateUpdate::CaptureAmountUpdate { amount_captured } => Self { mandate_status: None, amount_captured, connector_mandate_ids: None, + connector_mandate_id: None, + payment_method_id: None, + original_payment_id: None, }, MandateUpdate::ConnectorReferenceUpdate { - connector_mandate_ids: connector_mandate_id, + connector_mandate_ids, + } => Self { + connector_mandate_ids, + ..Default::default() + }, + MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id, + connector_mandate_ids, + payment_method_id, + original_payment_id, } => Self { - connector_mandate_ids: connector_mandate_id, + connector_mandate_id, + connector_mandate_ids, + payment_method_id: Some(payment_method_id), + original_payment_id, ..Default::default() }, } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index d08c146b0b88..4a7603384c55 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -51,7 +51,7 @@ pub struct PaymentAttempt { pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, // providing a location to store mandate details intermediately for transaction - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub multiple_capture_count: Option, // reference to the payment at connector side @@ -126,7 +126,7 @@ pub struct PaymentAttemptNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub multiple_capture_count: Option, diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs index 5a1ea696c56e..5a2226f06764 100644 --- a/crates/diesel_models/src/user/sample_data.rs +++ b/crates/diesel_models/src/user/sample_data.rs @@ -5,7 +5,7 @@ use common_enums::{ use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; -use crate::{enums::MandateDataType, schema::payment_attempt, PaymentAttemptNew}; +use crate::{enums::MandateTypeDetails, schema::payment_attempt, PaymentAttemptNew}; #[derive( Clone, Debug, Default, diesel::Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize, @@ -50,7 +50,7 @@ pub struct PaymentAttemptBatchNew { pub business_sub_label: Option, pub straight_through_algorithm: Option, pub preprocessing_step_id: Option, - pub mandate_details: Option, + pub mandate_details: Option, pub error_reason: Option, pub connector_response_reference_id: Option, pub connector_transaction_id: Option, diff --git a/crates/router/src/compatibility/stripe/payment_intents/types.rs b/crates/router/src/compatibility/stripe/payment_intents/types.rs index 810e0ed1d284..aac150b50798 100644 --- a/crates/router/src/compatibility/stripe/payment_intents/types.rs +++ b/crates/router/src/compatibility/stripe/payment_intents/types.rs @@ -753,6 +753,7 @@ impl ForeignTryFrom<(Option, Option)> for Option { - let profile_id = if let Some(ref payment_id) = mandate.original_payment_id { - let pi = db - .find_payment_intent_by_payment_id_merchant_id( - payment_id, - &merchant_account.merchant_id, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::PaymentNotFound)?; - let profile_id = pi.profile_id.clone().ok_or( - errors::ApiErrorResponse::BusinessProfileNotFound { - id: pi - .profile_id - .unwrap_or_else(|| "Profile id is Null".to_string()), - }, - )?; - Ok(profile_id) - } else { - Err(errors::ApiErrorResponse::PaymentNotFound) - }?; + let profile_id = + helpers::get_profile_id_for_mandate(&state, &merchant_account, mandate.clone()) + .await?; - let merchant_connector_account = helpers::get_merchant_connector_account( + let merchant_connector_account = payment_helper::get_merchant_connector_account( &state, &merchant_account.merchant_id, None, &key_store, &profile_id, - &mandate.connector, + &mandate.connector.clone(), mandate.merchant_connector_id.as_ref(), ) .await?; @@ -243,7 +226,72 @@ where _ => Some(router_data.request.get_payment_method_data()), } } +pub async fn update_mandate_procedure( + state: &AppState, + resp: types::RouterData, + mandate: Mandate, + merchant_id: &str, + pm_id: Option, +) -> errors::RouterResult> +where + FData: MandateBehaviour, +{ + let mandate_details = match &resp.response { + Ok(types::PaymentsResponseData::TransactionResponse { + mandate_reference, .. + }) => mandate_reference, + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Unexpected response received")?, + Err(_) => return Ok(resp), + }; + let old_record = payments::UpdateHistory { + connector_mandate_id: mandate.connector_mandate_id, + payment_method_id: mandate.payment_method_id, + original_payment_id: mandate.original_payment_id, + }; + + let mandate_ref = mandate + .connector_mandate_ids + .parse_value::("Connector Reference Id") + .change_context(errors::ApiErrorResponse::MandateDeserializationFailed)?; + + let mut update_history = mandate_ref.update_history.unwrap_or_default(); + update_history.push(old_record); + + let updated_mandate_ref = payments::ConnectorMandateReferenceId { + connector_mandate_id: mandate_details + .as_ref() + .and_then(|mandate_ref| mandate_ref.connector_mandate_id.clone()), + payment_method_id: pm_id.clone(), + update_history: Some(update_history), + }; + + let connector_mandate_ids = + Encode::::encode_to_value(&updated_mandate_ref) + .change_context(errors::ApiErrorResponse::InternalServerError) + .map(masking::Secret::new)?; + + let _update_mandate_details = state + .store + .update_mandate_by_merchant_id_mandate_id( + merchant_id, + &mandate.mandate_id, + diesel_models::MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id: mandate_details + .as_ref() + .and_then(|man_ref| man_ref.connector_mandate_id.clone()), + connector_mandate_ids: Some(connector_mandate_ids), + payment_method_id: pm_id + .unwrap_or("Error retrieving the payment_method_id".to_string()), + original_payment_id: Some(resp.payment_id.clone()), + }, + ) + .await + .change_context(errors::ApiErrorResponse::MandateUpdateFailed)?; + Ok(resp) +} pub async fn mandate_procedure( state: &AppState, mut resp: types::RouterData, @@ -324,7 +372,7 @@ where }) .transpose()?; - if let Some(new_mandate_data) = helpers::generate_mandate( + if let Some(new_mandate_data) = payment_helper::generate_mandate( resp.merchant_id.clone(), resp.payment_id.clone(), resp.connector.clone(), @@ -363,6 +411,8 @@ where api_models::payments::ConnectorMandateReferenceId { connector_mandate_id: connector_id.connector_mandate_id, payment_method_id: connector_id.payment_method_id, + update_history:None, + } ))) })); diff --git a/crates/router/src/core/mandate/helpers.rs b/crates/router/src/core/mandate/helpers.rs new file mode 100644 index 000000000000..150130ed9e5d --- /dev/null +++ b/crates/router/src/core/mandate/helpers.rs @@ -0,0 +1,35 @@ +use common_utils::errors::CustomResult; +use diesel_models::Mandate; +use error_stack::ResultExt; + +use crate::{core::errors, routes::AppState, types::domain}; + +pub async fn get_profile_id_for_mandate( + state: &AppState, + merchant_account: &domain::MerchantAccount, + mandate: Mandate, +) -> CustomResult { + let profile_id = if let Some(ref payment_id) = mandate.original_payment_id { + let pi = state + .store + .find_payment_intent_by_payment_id_merchant_id( + payment_id, + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let profile_id = + pi.profile_id + .clone() + .ok_or(errors::ApiErrorResponse::BusinessProfileNotFound { + id: pi + .profile_id + .unwrap_or_else(|| "Profile id is Null".to_string()), + })?; + Ok(profile_id) + } else { + Err(errors::ApiErrorResponse::PaymentNotFound) + }?; + Ok(profile_id) +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 3dbd6fcb215b..e08dbc61f5e7 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1823,14 +1823,24 @@ pub async fn list_payment_methods( } else { api_surcharge_decision_configs::MerchantSurchargeConfigs::default() }; + print!("PAMT{:?}", payment_attempt); Ok(services::ApplicationResponse::Json( api::PaymentMethodListResponse { redirect_url: merchant_account.return_url, merchant_name: merchant_account.merchant_name, payment_type, payment_methods: payment_method_responses, - mandate_payment: payment_attempt.and_then(|inner| inner.mandate_details).map( - |d| match d { + mandate_payment: payment_attempt + .and_then(|inner| inner.mandate_details) + .and_then(|man_type_details| match man_type_details { + data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => { + Some(mandate_type) + } + data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => { + mandate_details.mandate_type + } + }) + .map(|d| match d { data_models::mandates::MandateDataType::SingleUse(i) => { api::MandateType::SingleUse(api::MandateAmountData { amount: i.amount, @@ -1852,8 +1862,7 @@ pub async fn list_payment_methods( data_models::mandates::MandateDataType::MultiUse(None) => { api::MandateType::MultiUse(None) } - }, - ), + }), show_surcharge_breakup_screen: merchant_surcharge_configs .show_surcharge_breakup_screen .unwrap_or_default(), diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index d6343ed871b0..8b0b54158fd0 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; +use error_stack::{IntoReport, ResultExt}; use super::{ConstructFlowSpecificData, Feature}; use crate::{ core::{ - errors::{self, ConnectorErrorExt, RouterResult}, + errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt}, mandate, payments::{ self, access_token, customers, helpers, tokenization, transformers, PaymentData, @@ -65,16 +66,16 @@ impl Feature for types::Setup types::SetupMandateRequestData, types::PaymentsResponseData, > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( state, connector_integration, &self, - call_connector_action, + call_connector_action.clone(), connector_request, ) .await .to_setup_mandate_failed_response()?; - let pm_id = Box::pin(tokenization::save_payment_method( state, connector, @@ -86,14 +87,84 @@ impl Feature for types::Setup )) .await?; - mandate::mandate_procedure( - state, - resp, - maybe_customer, - pm_id, - connector.merchant_connector_id.clone(), - ) - .await + if let Some(mandate_id) = self + .request + .setup_mandate_details + .as_ref() + .and_then(|mandate_data| mandate_data.update_mandate_id.clone()) + { + let mandate = state + .store + .find_mandate_by_merchant_id_mandate_id(&merchant_account.merchant_id, &mandate_id) + .await + .to_not_found_response(errors::ApiErrorResponse::MandateNotFound)?; + + let profile_id = mandate::helpers::get_profile_id_for_mandate( + state, + merchant_account, + mandate.clone(), + ) + .await?; + match resp.response { + Ok(types::PaymentsResponseData::TransactionResponse { .. }) => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + types::api::MandateRevoke, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + > = connector.connector.get_connector_integration(); + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + &merchant_account.merchant_id, + None, + key_store, + &profile_id, + &mandate.connector, + mandate.merchant_connector_id.as_ref(), + ) + .await?; + + let router_data = mandate::utils::construct_mandate_revoke_router_data( + merchant_connector_account, + merchant_account, + mandate.clone(), + ) + .await?; + + let _response = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + call_connector_action, + None, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + // TODO:Add the revoke mandate task to process tracker + mandate::update_mandate_procedure( + state, + resp, + mandate, + &merchant_account.merchant_id, + pm_id, + ) + .await + } + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Unexpected response received")?, + Err(_) => Ok(resp), + } + } else { + mandate::mandate_procedure( + state, + resp, + maybe_customer, + pm_id, + connector.merchant_connector_id.clone(), + ) + .await + } } async fn add_access_token<'a>( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0cbed255348e..520582eb22fb 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -840,7 +840,7 @@ fn validate_new_mandate_request( let mandate_details = match mandate_data.mandate_type { Some(api_models::payments::MandateType::SingleUse(details)) => Some(details), Some(api_models::payments::MandateType::MultiUse(details)) => details, - None => None, + _ => None, }; mandate_details.and_then(|md| md.start_date.zip(md.end_date)).map(|(start_date, end_date)| utils::when (start_date >= end_date, || { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index c81145c5de72..14fc28d67237 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -431,7 +431,29 @@ impl // The operation merges mandate data from both request and payment_attempt setup_mandate = setup_mandate.map(|mut sm| { - sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); + sm.mandate_type = payment_attempt + .mandate_details + .clone() + .and_then(|mandate| match mandate { + data_models::mandates::MandateTypeDetails::MandateType(mandate_type) => { + Some(mandate_type) + } + data_models::mandates::MandateTypeDetails::MandateDetails(mandate_details) => { + mandate_details.mandate_type + } + }) + .or(sm.mandate_type); + sm.update_mandate_id = payment_attempt + .mandate_details + .clone() + .and_then(|mandate| match mandate { + data_models::mandates::MandateTypeDetails::MandateType(_) => None, + data_models::mandates::MandateTypeDetails::MandateDetails(update_id) => { + Some(update_id.update_mandate_id) + } + }) + .flatten() + .or(sm.update_mandate_id); sm }); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2b25a74deb19..d02ad15fbd64 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -3,7 +3,10 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; -use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; +use data_models::{ + mandates::{MandateData, MandateDetails, MandateTypeDetails}, + payments::payment_attempt::PaymentAttempt, +}; use diesel_models::ephemeral_key; use error_stack::{self, ResultExt}; use masking::PeekInterface; @@ -255,7 +258,7 @@ impl .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { payment_id: payment_id.clone(), })?; - + // connector mandate reference update history let mandate_id = request .mandate_id .as_ref() @@ -284,10 +287,11 @@ impl api_models::payments::MandateIds { mandate_id: mandate_obj.mandate_id, mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - api_models::payments::ConnectorMandateReferenceId { - connector_mandate_id: connector_id.connector_mandate_id, - payment_method_id: connector_id.payment_method_id, - }, + api_models::payments::ConnectorMandateReferenceId{ + connector_mandate_id: connector_id.connector_mandate_id, + payment_method_id: connector_id.payment_method_id, + update_history: None + } )) } }), @@ -701,6 +705,35 @@ impl PaymentCreate { .surcharge_details .and_then(|surcharge_details| surcharge_details.tax_amount); + if request.mandate_data.as_ref().map_or(false, |mandate_data| { + mandate_data.update_mandate_id.is_some() && mandate_data.mandate_type.is_some() + }) { + Err(errors::ApiErrorResponse::InvalidRequestData {message:"Only one field out of 'mandate_type' and 'update_mandate_id' was expected, found both".to_string()})? + } + + let mandate_dets = if let Some(update_id) = request + .mandate_data + .as_ref() + .and_then(|inner| inner.update_mandate_id.clone()) + { + let mandate_data = MandateDetails { + update_mandate_id: Some(update_id), + mandate_type: None, + }; + Some(MandateTypeDetails::MandateDetails(mandate_data)) + } else { + // let mandate_type: data_models::mandates::MandateDataType = + + let mandate_data = MandateDetails { + update_mandate_id: None, + mandate_type: request + .mandate_data + .as_ref() + .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + }; + Some(MandateTypeDetails::MandateDetails(mandate_data)) + }; + Ok(( storage::PaymentAttemptNew { payment_id: payment_id.to_string(), @@ -727,10 +760,7 @@ impl PaymentCreate { business_sub_label: request.business_sub_label.clone(), surcharge_amount, tax_amount, - mandate_details: request - .mandate_data - .as_ref() - .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + mandate_details: mandate_dets, ..storage::PaymentAttemptNew::default() }, additional_pm_data, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e002b92d1810..015ef5cea6ef 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -255,10 +255,7 @@ impl api_models::payments::MandateIds { mandate_id: mandate_obj.mandate_id, mandate_reference_id: Some(api_models::payments::MandateReferenceId::ConnectorMandateId( - api_models::payments::ConnectorMandateReferenceId { - connector_mandate_id: connector_id.connector_mandate_id, - payment_method_id: connector_id.payment_method_id, - }, + api_models::payments::ConnectorMandateReferenceId {connector_mandate_id:connector_id.connector_mandate_id,payment_method_id:connector_id.payment_method_id, update_history: None }, )) } }), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5f0c702a29d9..61917fdcd2e6 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -636,6 +636,7 @@ where api::MandateType::MultiUse(None) } }), + update_mandate_id: d.update_mandate_id, }), auth_flow == services::AuthFlow::Merchant, ) diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index fcd71719657b..0cf5cabf2e42 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -202,6 +202,18 @@ impl MandateInterface for MockDb { } => { mandate.connector_mandate_ids = connector_mandate_ids; } + + diesel_models::MandateUpdate::ConnectorMandateIdUpdate { + connector_mandate_id, + connector_mandate_ids, + payment_method_id, + original_payment_id, + } => { + mandate.connector_mandate_ids = connector_mandate_ids; + mandate.connector_mandate_id = connector_mandate_id; + mandate.payment_method_id = payment_method_id; + mandate.original_payment_id = original_payment_id + } } Ok(mandate.clone()) } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 307dff550717..bc8ab2e05e78 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1674,7 +1674,7 @@ pub fn build_redirection_form( // Initialize the ThreeDSService const threeDS = gateway.get3DSecure(); - + const options = {{ customerVaultId: '{customer_vault_id}', currency: '{currency}', diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 786a8c551824..41aefc9026d2 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -323,6 +323,7 @@ impl ForeignFrom for data_models::mandates::M data_models::mandates::MandateDataType::MultiUse(None) } }), + update_mandate_id: d.update_mandate_id, } } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index f8f752c6bc8d..b8d71cb32b7d 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -2,7 +2,7 @@ use api_models::enums::{AuthenticationType, Connector, PaymentMethod, PaymentMet use common_utils::{errors::CustomResult, fallback_reverse_lookup_not_found}; use data_models::{ errors, - mandates::{MandateAmountData, MandateDataType}, + mandates::{MandateAmountData, MandateDataType, MandateDetails, MandateTypeDetails}, payments::{ payment_attempt::{ PaymentAttempt, PaymentAttemptInterface, PaymentAttemptNew, PaymentAttemptUpdate, @@ -14,6 +14,7 @@ use data_models::{ use diesel_models::{ enums::{ MandateAmountData as DieselMandateAmountData, MandateDataType as DieselMandateType, + MandateDetails as DieselMandateDetails, MandateTypeDetails as DieselMandateTypeOrDetails, MerchantStorageScheme, }, kv, @@ -999,6 +1000,50 @@ impl DataModelExt for MandateAmountData { } } } +impl DataModelExt for MandateDetails { + type StorageModel = DieselMandateDetails; + fn to_storage_model(self) -> Self::StorageModel { + DieselMandateDetails { + update_mandate_id: self.update_mandate_id, + mandate_type: self + .mandate_type + .map(|mand_type| mand_type.to_storage_model()), + } + } + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + update_mandate_id: storage_model.update_mandate_id, + mandate_type: storage_model + .mandate_type + .map(MandateDataType::from_storage_model), + } + } +} +impl DataModelExt for MandateTypeDetails { + type StorageModel = DieselMandateTypeOrDetails; + + fn to_storage_model(self) -> Self::StorageModel { + match self { + Self::MandateType(mandate_type) => { + DieselMandateTypeOrDetails::MandateType(mandate_type.to_storage_model()) + } + Self::MandateDetails(mandate_details) => { + DieselMandateTypeOrDetails::MandateDetails(mandate_details.to_storage_model()) + } + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + match storage_model { + DieselMandateTypeOrDetails::MandateType(data) => { + Self::MandateType(MandateDataType::from_storage_model(data)) + } + DieselMandateTypeOrDetails::MandateDetails(data) => { + Self::MandateDetails(MandateDetails::from_storage_model(data)) + } + } + } +} impl DataModelExt for MandateDataType { type StorageModel = DieselMandateType; @@ -1123,7 +1168,7 @@ impl DataModelExt for PaymentAttempt { preprocessing_step_id: storage_model.preprocessing_step_id, mandate_details: storage_model .mandate_details - .map(MandateDataType::from_storage_model), + .map(MandateTypeDetails::from_storage_model), error_reason: storage_model.error_reason, multiple_capture_count: storage_model.multiple_capture_count, connector_response_reference_id: storage_model.connector_response_reference_id, @@ -1231,7 +1276,7 @@ impl DataModelExt for PaymentAttemptNew { preprocessing_step_id: storage_model.preprocessing_step_id, mandate_details: storage_model .mandate_details - .map(MandateDataType::from_storage_model), + .map(MandateTypeDetails::from_storage_model), error_reason: storage_model.error_reason, connector_response_reference_id: storage_model.connector_response_reference_id, multiple_capture_count: storage_model.multiple_capture_count, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 7858029961a8..09cb5fe14040 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9026,6 +9026,11 @@ "MandateData": { "type": "object", "properties": { + "update_mandate_id": { + "type": "string", + "description": "A way to update the mandate's payment method details", + "nullable": true + }, "customer_acceptance": { "allOf": [ { From d91da89065a6870f05e1ff9db007d16a58454c84 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:43:13 +0530 Subject: [PATCH 403/443] feat(users): Signin and Verify Email changes for User Invitation changes (#3420) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 7 +- crates/api_models/src/user.rs | 19 +++- crates/router/src/core/errors/user.rs | 6 ++ crates/router/src/core/user.rs | 119 +++++++++++++++++++--- crates/router/src/routes/app.rs | 35 +++++-- crates/router/src/routes/lock_utils.rs | 2 + crates/router/src/routes/user.rs | 38 +++++++ crates/router/src/routes/user_role.rs | 2 +- crates/router/src/types/domain/user.rs | 134 ++++++++++++++++++++++++- crates/router/src/utils/user.rs | 30 +++++- crates/router/src/utils/user_role.rs | 14 ++- crates/router_env/src/logger/types.rs | 4 + 12 files changed, 369 insertions(+), 41 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 40d082d1cade..04aabc071aea 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -12,9 +12,9 @@ use crate::user::{ }, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, - InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignUpRequest, - SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, - UserMerchantCreate, VerifyEmailRequest, + InviteUserResponse, ResetPasswordRequest, SendVerifyEmailRequest, SignInResponse, + SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, + UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest, }; impl ApiEventMetric for DashboardEntryResponse { @@ -56,6 +56,7 @@ common_utils::impl_misc_api_event_type!( InviteUserResponse, VerifyEmailRequest, SendVerifyEmailRequest, + SignInResponse, UpdateUserAccountDetailsRequest ); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 056d1b593dc9..89f42f58c397 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -39,7 +39,21 @@ pub struct DashboardEntryResponse { pub type SignInRequest = SignUpRequest; -pub type SignInResponse = DashboardEntryResponse; +#[derive(Debug, serde::Serialize)] +#[serde(tag = "flow_type", rename_all = "snake_case")] +pub enum SignInResponse { + MerchantSelect(MerchantSelectResponse), + DashboardEntry(DashboardEntryResponse), +} + +#[derive(Debug, serde::Serialize)] +pub struct MerchantSelectResponse { + pub token: Secret, + pub name: Secret, + pub email: pii::Email, + pub verification_days_left: Option, + pub merchants: Vec, +} #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] pub struct ConnectAccountRequest { @@ -138,7 +152,7 @@ pub struct VerifyEmailRequest { pub token: Secret, } -pub type VerifyEmailResponse = DashboardEntryResponse; +pub type VerifyEmailResponse = SignInResponse; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct SendVerifyEmailRequest { @@ -149,6 +163,7 @@ pub struct SendVerifyEmailRequest { pub struct UserMerchantAccount { pub merchant_id: String, pub merchant_name: OptionalEncryptableName, + pub is_active: bool, } #[cfg(feature = "recon")] diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 389cb10d7b53..d3b1679378ea 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -58,6 +58,8 @@ pub enum UserErrors { InvalidDeleteOperation, #[error("MaxInvitationsError")] MaxInvitationsError, + #[error("RoleNotFound")] + RoleNotFound, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -146,6 +148,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 31, self.get_error_message(), None)) } + Self::RoleNotFound => { + AER::BadRequest(ApiError::new(sub_code, 32, self.get_error_message(), None)) + } } } } @@ -178,6 +183,7 @@ impl UserErrors { Self::ChangePasswordError => "Old and new password cannot be the same", Self::InvalidDeleteOperation => "Delete Operation Not Supported", Self::MaxInvitationsError => "Maximum invite count per request exceeded", + Self::RoleNotFound => "Role Not Found", } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index c2ed78f86658..ae66728e140f 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -97,10 +97,10 @@ pub async fn signup( )) } -pub async fn signin( +pub async fn signin_without_invite_checks( state: AppState, request: user_api::SignInRequest, -) -> UserResponse { +) -> UserResponse { let user_from_db: domain::UserFromStorage = state .store .find_user_by_email(request.email.clone().expose().expose().as_str()) @@ -124,6 +124,50 @@ pub async fn signin( )) } +pub async fn signin( + state: AppState, + request: user_api::SignInRequest, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidCredentials) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + user_from_db.compare_password(request.password)?; + + let signin_strategy = + if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() { + let preferred_role = user_from_db + .get_role_from_db_by_merchant_id(&state, preferred_merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("User role with preferred_merchant_id not found")?; + domain::SignInWithRoleStrategyType::SingleRole(domain::SignInWithSingleRoleStrategy { + user: user_from_db, + user_role: preferred_role, + }) + } else { + let user_roles = user_from_db.get_roles_from_db(&state).await?; + domain::SignInWithRoleStrategyType::decide_signin_strategy_by_user_roles( + user_from_db, + user_roles, + ) + .await? + }; + + Ok(ApplicationResponse::Json( + signin_strategy.get_signin_response(&state).await?, + )) +} + #[cfg(feature = "email")] pub async fn connect_account( state: AppState, @@ -832,22 +876,22 @@ pub async fn list_merchant_ids_for_user( state: AppState, user: auth::UserFromToken, ) -> UserResponse> { - let merchant_ids = utils::user_role::get_merchant_ids_for_user(&state, &user.user_id).await?; + let user_roles = + utils::user_role::get_active_user_roles_for_user(&state, &user.user_id).await?; let merchant_accounts = state .store - .list_multiple_merchant_accounts(merchant_ids) + .list_multiple_merchant_accounts( + user_roles + .iter() + .map(|role| role.merchant_id.clone()) + .collect(), + ) .await .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::Json( - merchant_accounts - .into_iter() - .map(|acc| user_api::UserMerchantAccount { - merchant_id: acc.merchant_id, - merchant_name: acc.merchant_name, - }) - .collect(), + utils::user::get_multiple_merchant_details_with_status(user_roles, merchant_accounts)?, )) } @@ -868,11 +912,38 @@ pub async fn get_users_for_merchant_account( Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) } +#[cfg(feature = "email")] +pub async fn verify_email_without_invite_checks( + state: AppState, + req: user_api::VerifyEmailRequest, +) -> UserResponse { + let token = auth::decode_jwt::(&req.token.clone().expose(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + let user = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)?; + let user = state + .store + .update_user_by_user_id(user.user_id.as_str(), storage_user::UserUpdate::VerifyUser) + .await + .change_context(UserErrors::InternalServerError)?; + let user_from_db: domain::UserFromStorage = user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + )) +} + #[cfg(feature = "email")] pub async fn verify_email( state: AppState, req: user_api::VerifyEmailRequest, -) -> UserResponse { +) -> UserResponse { let token = auth::decode_jwt::(&req.token.clone().expose(), &state) .await .change_context(UserErrors::LinkInvalid)?; @@ -890,11 +961,29 @@ pub async fn verify_email( .change_context(UserErrors::InternalServerError)?; let user_from_db: domain::UserFromStorage = user.into(); - let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; + + let signin_strategy = + if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() { + let preferred_role = user_from_db + .get_role_from_db_by_merchant_id(&state, preferred_merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("User role with preferred_merchant_id not found")?; + domain::SignInWithRoleStrategyType::SingleRole(domain::SignInWithSingleRoleStrategy { + user: user_from_db, + user_role: preferred_role, + }) + } else { + let user_roles = user_from_db.get_roles_from_db(&state).await?; + domain::SignInWithRoleStrategyType::decide_signin_strategy_by_user_roles( + user_from_db, + user_roles, + ) + .await? + }; Ok(ApplicationResponse::Json( - utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, + signin_strategy.get_signin_response(&state).await?, )) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f490cee8dab3..e18e4d85c704 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -952,7 +952,10 @@ impl User { let mut route = web::scope("/user").app_data(web::Data::new(state)); route = route - .service(web::resource("/signin").route(web::post().to(user_signin))) + .service( + web::resource("/signin").route(web::post().to(user_signin_without_invite_checks)), + ) + .service(web::resource("/v2/signin").route(web::post().to(user_signin))) .service(web::resource("/change_password").route(web::post().to(change_password))) .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) @@ -961,14 +964,7 @@ impl User { .route(web::post().to(user_merchant_account_create)), ) .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) - .service(web::resource("/user/list").route(web::get().to(get_user_details))) .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) - .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) - .service(web::resource("/role/list").route(web::get().to(list_roles))) - .service(web::resource("/role").route(web::get().to(get_role_from_token))) - .service(web::resource("/role/{role_id}").route(web::get().to(get_role))) - .service(web::resource("/user/invite").route(web::post().to(invite_user))) - .service(web::resource("/user/invite/accept").route(web::post().to(accept_invitation))) .service(web::resource("/update").route(web::post().to(update_user_account_details))) .service( web::resource("/user/invite_multiple").route(web::post().to(invite_multiple_user)), @@ -980,6 +976,23 @@ impl User { ) .service(web::resource("/user/delete").route(web::delete().to(delete_user_role))); + // User management + route = route.service( + web::scope("/user") + .service(web::resource("/list").route(web::get().to(get_user_details))) + .service(web::resource("/invite").route(web::post().to(invite_user))) + .service(web::resource("/invite/accept").route(web::post().to(accept_invitation))) + .service(web::resource("/update_role").route(web::post().to(update_user_role))), + ); + + // Role information + route = route.service( + web::scope("/role") + .service(web::resource("").route(web::get().to(get_role_from_token))) + .service(web::resource("/list").route(web::get().to(list_all_roles))) + .service(web::resource("/{role_id}").route(web::get().to(get_role))), + ); + #[cfg(feature = "dummy_connector")] { route = route.service( @@ -1000,7 +1013,11 @@ impl User { web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), ) - .service(web::resource("/verify_email").route(web::post().to(verify_email))) + .service( + web::resource("/verify_email") + .route(web::post().to(verify_email_without_invite_checks)), + ) + .service(web::resource("/v2/verify_email").route(web::post().to(verify_email))) .service( web::resource("/verify_email_request") .route(web::post().to(verify_email_request)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6df8c7fb7a7c..07894afe7323 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -161,6 +161,7 @@ impl From for ApiIdentifier { Flow::UserConnectAccount | Flow::UserSignUp + | Flow::UserSignInWithoutInviteChecks | Flow::UserSignIn | Flow::ChangePassword | Flow::SetDashboardMetadata @@ -179,6 +180,7 @@ impl From for ApiIdentifier { | Flow::InviteMultipleUser | Flow::DeleteUser | Flow::UserSignUpWithMerchantId + | Flow::VerifyEmailWithoutInviteChecks | Flow::VerifyEmail | Flow::VerifyEmailRequest | Flow::UpdateUserAccountDetails => Self::User, diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 02704cf701f5..88e19ddf7550 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -58,6 +58,25 @@ pub async fn user_signup( .await } +pub async fn user_signin_without_invite_checks( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignInWithoutInviteChecks; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signin_without_invite_checks(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn user_signin( state: web::Data, http_req: HttpRequest, @@ -368,6 +387,25 @@ pub async fn invite_multiple_user( .await } +#[cfg(feature = "email")] +pub async fn verify_email_without_invite_checks( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::VerifyEmailWithoutInviteChecks; + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + json_payload.into_inner(), + |state, _, req_payload| user_core::verify_email_without_invite_checks(state, req_payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "email")] pub async fn verify_email( state: web::Data, diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index f83134e58251..3f9ccda8651f 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -29,7 +29,7 @@ pub async fn get_authorization_info( .await } -pub async fn list_roles(state: web::Data, req: HttpRequest) -> HttpResponse { +pub async fn list_all_roles(state: web::Data, req: HttpRequest) -> HttpResponse { let flow = Flow::ListRoles; Box::pin(api::server_wrap( flow, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index bbe21f289aa1..d3ea69ecd863 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -26,11 +26,12 @@ use crate::{ db::StorageInterface, routes::AppState, services::{ + authentication as auth, authentication::UserFromToken, authorization::{info, predefined_permissions}, }, types::transformers::ForeignFrom, - utils::user::password, + utils::{self, user::password}, }; pub mod dashboard_metadata; @@ -733,7 +734,15 @@ impl UserFromStorage { pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store - .find_user_role_by_user_id(self.get_user_id()) + .find_user_role_by_user_id(&self.0.user_id) + .await + .change_context(UserErrors::InternalServerError) + } + + pub async fn get_roles_from_db(&self, state: &AppState) -> UserResult> { + state + .store + .list_user_roles_by_user_id(&self.0.user_id) .await .change_context(UserErrors::InternalServerError) } @@ -760,6 +769,29 @@ impl UserFromStorage { let days_left_for_verification = last_date_for_verification - today; Ok(Some(days_left_for_verification.whole_days())) } + + pub fn get_preferred_merchant_id(&self) -> Option { + self.0.preferred_merchant_id.clone() + } + + pub async fn get_role_from_db_by_merchant_id( + &self, + state: &AppState, + merchant_id: &str, + ) -> UserResult { + state + .store + .find_user_role_by_user_id_merchant_id(self.get_user_id(), merchant_id) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + UserErrors::RoleNotFound + } else { + UserErrors::InternalServerError + } + }) + .into_report() + } } impl From for user_role_api::ModuleInfo { @@ -828,3 +860,101 @@ impl TryFrom for user_api::UserDetails { }) } } + +pub enum SignInWithRoleStrategyType { + SingleRole(SignInWithSingleRoleStrategy), + MultipleRoles(SignInWithMultipleRolesStrategy), +} + +impl SignInWithRoleStrategyType { + pub async fn decide_signin_strategy_by_user_roles( + user: UserFromStorage, + user_roles: Vec, + ) -> UserResult { + if user_roles.is_empty() { + return Err(UserErrors::InternalServerError.into()); + } + + if let Some(user_role) = user_roles + .iter() + .find(|role| role.status == UserStatus::Active) + { + Ok(Self::SingleRole(SignInWithSingleRoleStrategy { + user, + user_role: user_role.clone(), + })) + } else { + Ok(Self::MultipleRoles(SignInWithMultipleRolesStrategy { + user, + user_roles, + })) + } + } + + pub async fn get_signin_response( + self, + state: &AppState, + ) -> UserResult { + match self { + Self::SingleRole(strategy) => strategy.get_signin_response(state).await, + Self::MultipleRoles(strategy) => strategy.get_signin_response(state).await, + } + } +} + +pub struct SignInWithSingleRoleStrategy { + pub user: UserFromStorage, + pub user_role: UserRole, +} + +impl SignInWithSingleRoleStrategy { + async fn get_signin_response(self, state: &AppState) -> UserResult { + let token = + utils::user::generate_jwt_auth_token(state, &self.user, &self.user_role).await?; + let dashboard_entry_response = + utils::user::get_dashboard_entry_response(state, self.user, self.user_role, token)?; + Ok(user_api::SignInResponse::DashboardEntry( + dashboard_entry_response, + )) + } +} + +pub struct SignInWithMultipleRolesStrategy { + pub user: UserFromStorage, + pub user_roles: Vec, +} + +impl SignInWithMultipleRolesStrategy { + async fn get_signin_response(self, state: &AppState) -> UserResult { + let merchant_accounts = state + .store + .list_multiple_merchant_accounts( + self.user_roles + .iter() + .map(|role| role.merchant_id.clone()) + .collect(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let merchant_details = utils::user::get_multiple_merchant_details_with_status( + self.user_roles, + merchant_accounts, + )?; + + Ok(user_api::SignInResponse::MerchantSelect( + user_api::MerchantSelectResponse { + name: self.user.get_name(), + email: self.user.get_email(), + token: auth::UserAuthToken::new_token( + self.user.get_user_id().to_string(), + &state.conf, + ) + .await? + .into(), + merchants: merchant_details, + verification_days_left: utils::user::get_verification_days_left(state, &self.user)?, + }, + )) + } +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index a3f9e7978aa1..697d10f772e7 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use api_models::user as user_api; -use diesel_models::user_role::UserRole; +use diesel_models::{enums::UserStatus, user_role::UserRole}; use error_stack::ResultExt; use masking::Secret; @@ -118,3 +120,29 @@ pub fn get_verification_days_left( #[cfg(not(feature = "email"))] return Ok(None); } + +pub fn get_multiple_merchant_details_with_status( + user_roles: Vec, + merchant_accounts: Vec, +) -> UserResult> { + let roles: HashMap<_, _> = user_roles + .into_iter() + .map(|user_role| (user_role.merchant_id.clone(), user_role)) + .collect(); + + merchant_accounts + .into_iter() + .map(|merchant| { + let role = roles + .get(merchant.merchant_id.as_str()) + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Merchant exists but user role doesn't")?; + + Ok(user_api::UserMerchantAccount { + merchant_id: merchant.merchant_id.clone(), + merchant_name: merchant.merchant_name.clone(), + is_active: role.status == UserStatus::Active, + }) + }) + .collect() +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index 65ead92ad347..7ca06aeda0d1 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -1,5 +1,5 @@ use api_models::user_role as user_role_api; -use diesel_models::enums::UserStatus; +use diesel_models::{enums::UserStatus, user_role::UserRole}; use error_stack::ResultExt; use crate::{ @@ -17,19 +17,17 @@ pub fn is_internal_role(role_id: &str) -> bool { || role_id == consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER } -pub async fn get_merchant_ids_for_user(state: &AppState, user_id: &str) -> UserResult> { +pub async fn get_active_user_roles_for_user( + state: &AppState, + user_id: &str, +) -> UserResult> { Ok(state .store .list_user_roles_by_user_id(user_id) .await .change_context(UserErrors::InternalServerError)? .into_iter() - .filter_map(|ele| { - if ele.status == UserStatus::Active { - return Some(ele.merchant_id); - } - None - }) + .filter(|ele| ele.status == UserStatus::Active) .collect()) } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 998c52f2c138..0d5710820ee6 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -267,6 +267,8 @@ pub enum Flow { UserSignUp, /// User Sign Up UserSignUpWithMerchantId, + /// User Sign In without invite checks + UserSignInWithoutInviteChecks, /// User Sign In UserSignIn, /// User connect account @@ -333,6 +335,8 @@ pub enum Flow { SyncOnboardingStatus, /// Reset tracking id ResetTrackingId, + /// Verify email token without invite checks + VerifyEmailWithoutInviteChecks, /// Verify email Token VerifyEmail, /// Send verify email From d6807abba46136eabadcbfbc51bce421144dca2c Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:43:56 +0530 Subject: [PATCH 404/443] chore(analytics): adding status code to connector Kafka events (#3393) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sampras lopes --- crates/router/src/events/connector_api_logs.rs | 3 +++ crates/router/src/services/api.rs | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/router/src/events/connector_api_logs.rs b/crates/router/src/events/connector_api_logs.rs index 871a7af0d772..45c05a3077fd 100644 --- a/crates/router/src/events/connector_api_logs.rs +++ b/crates/router/src/events/connector_api_logs.rs @@ -18,6 +18,7 @@ pub struct ConnectorEvent { created_at: i128, request_id: String, latency: u128, + status_code: u16, } impl ConnectorEvent { @@ -33,6 +34,7 @@ impl ConnectorEvent { merchant_id: String, request_id: Option<&RequestId>, latency: u128, + status_code: u16, ) -> Self { Self { connector_name, @@ -52,6 +54,7 @@ impl ConnectorEvent { .map(|i| i.as_hyphenated().to_string()) .unwrap_or("NO_REQUEST_ID".to_string()), latency, + status_code, } } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index bc8ab2e05e78..e0c6a0862579 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -372,7 +372,13 @@ where let response = call_connector_api(state, request).await; let external_latency = current_time.elapsed().as_millis(); logger::debug!(connector_response=?response); - + let status_code = response + .as_ref() + .map(|i| { + i.as_ref() + .map_or_else(|value| value.status_code, |value| value.status_code) + }) + .unwrap_or_default(); let connector_event = ConnectorEvent::new( req.connector.clone(), std::any::type_name::(), @@ -394,6 +400,7 @@ where req.merchant_id.clone(), state.request_id.as_ref(), external_latency, + status_code, ); match connector_event.try_into() { From 937aea906e759e6e8a76a424db99ed052d46b7d2 Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:53:58 +0530 Subject: [PATCH 405/443] chore(analytics): adding dispute id to api log events (#3450) --- .../cluster_setup/scripts/api_event_logs.sql | 17 ++++++++----- .../docs/clickhouse/scripts/api_events.sql | 10 +++++--- crates/api_models/src/events.rs | 3 +-- crates/api_models/src/events/dispute.rs | 25 +++++++++++++++++++ crates/common_utils/src/events.rs | 3 +++ crates/router/src/events/api_logs.rs | 9 ++++++- 6 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 crates/api_models/src/events/dispute.rs diff --git a/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql index 0fe194a0e676..ad0fe6d778fd 100644 --- a/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql +++ b/crates/analytics/docs/clickhouse/cluster_setup/scripts/api_event_logs.sql @@ -18,7 +18,8 @@ CREATE TABLE hyperswitch.api_events_queue on cluster '{cluster}' ( `created_at` DateTime CODEC(T64, LZ4), `latency` Nullable(UInt128), `user_agent` Nullable(String), - `ip_addr` Nullable(String) + `ip_addr` Nullable(String), + `dispute_id` Nullable(String) ) ENGINE = Kafka SETTINGS kafka_broker_list = 'hyper-c1-kafka-brokers.kafka-cluster.svc.cluster.local:9092', kafka_topic_list = 'hyperswitch-api-log-events', kafka_group_name = 'hyper-c1', @@ -81,7 +82,8 @@ CREATE TABLE hyperswitch.api_events_dist on cluster '{cluster}' ( `created_at` DateTime64(3), `latency` Nullable(UInt128), `user_agent` Nullable(String), - `ip_addr` Nullable(String) + `ip_addr` Nullable(String), + `dispute_id` Nullable(String) ) ENGINE = Distributed('{cluster}', 'hyperswitch', 'api_events_clustered', rand()); CREATE MATERIALIZED VIEW hyperswitch.api_events_mv on cluster '{cluster}' TO hyperswitch.api_events_dist ( @@ -105,7 +107,8 @@ CREATE MATERIALIZED VIEW hyperswitch.api_events_mv on cluster '{cluster}' TO hyp `created_at` DateTime64(3), `latency` Nullable(UInt128), `user_agent` Nullable(String), - `ip_addr` Nullable(String) + `ip_addr` Nullable(String), + `dispute_id` Nullable(String) ) AS SELECT merchant_id, @@ -158,7 +161,7 @@ WHERE length(_error) > 0 ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `url_path` LowCardinality(Nullable(String)); ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `event_type` LowCardinality(Nullable(String)); - +ALTER TABLE hyperswitch.api_events_clustered on cluster '{cluster}' ADD COLUMN `dispute_id` Nullable(String); CREATE TABLE hyperswitch.api_audit_log ON CLUSTER '{cluster}' ( `merchant_id` LowCardinality(String), @@ -209,7 +212,8 @@ CREATE MATERIALIZED VIEW hyperswitch.api_audit_log_mv ON CLUSTER `{cluster}` TO `created_at` DateTime64(3), `latency` Nullable(UInt128), `user_agent` Nullable(String), - `ip_addr` Nullable(String) + `ip_addr` Nullable(String), + `dispute_id` Nullable(String) ) AS SELECT merchant_id, @@ -232,6 +236,7 @@ SELECT created_at, latency, user_agent, - ip_addr + ip_addr, + dispute_id FROM hyperswitch.api_events_queue WHERE length(_error) = 0 \ No newline at end of file diff --git a/crates/analytics/docs/clickhouse/scripts/api_events.sql b/crates/analytics/docs/clickhouse/scripts/api_events.sql index c3fc3d7b06d5..49a6472eaa4d 100644 --- a/crates/analytics/docs/clickhouse/scripts/api_events.sql +++ b/crates/analytics/docs/clickhouse/scripts/api_events.sql @@ -23,7 +23,8 @@ CREATE TABLE api_events_queue ( `ip_addr` String, `hs_latency` Nullable(UInt128), `http_method` LowCardinality(String), - `url_path` String + `url_path` String, + `dispute_id` Nullable(String) ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-api-log-events', kafka_group_name = 'hyper-c1', @@ -57,6 +58,7 @@ CREATE TABLE api_events_dist ( `hs_latency` Nullable(UInt128), `http_method` LowCardinality(String), `url_path` String, + `dispute_id` Nullable(String) INDEX flowIndex flow_type TYPE bloom_filter GRANULARITY 1, INDEX apiIndex api_flow TYPE bloom_filter GRANULARITY 1, INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 @@ -92,7 +94,8 @@ CREATE MATERIALIZED VIEW api_events_mv TO api_events_dist ( `ip_addr` String, `hs_latency` Nullable(UInt128), `http_method` LowCardinality(String), - `url_path` String + `url_path` String, + `dispute_id` Nullable(String) ) AS SELECT merchant_id, @@ -120,7 +123,8 @@ SELECT ip_addr, hs_latency, http_method, - url_path + url_path, + dispute_id FROM api_events_queue where length(_error) = 0; diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index a8185d2d241c..ae0525ac6098 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,5 +1,6 @@ pub mod connector_onboarding; pub mod customer; +pub mod dispute; pub mod gsm; mod locker_migration; pub mod payment; @@ -44,8 +45,6 @@ impl_misc_api_event_type!( RetrievePaymentLinkResponse, MandateListConstraints, CreateFileResponse, - DisputeResponse, - SubmitEvidenceRequest, MerchantConnectorResponse, MerchantConnectorId, MandateResponse, diff --git a/crates/api_models/src/events/dispute.rs b/crates/api_models/src/events/dispute.rs new file mode 100644 index 000000000000..101dba3ca028 --- /dev/null +++ b/crates/api_models/src/events/dispute.rs @@ -0,0 +1,25 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use super::{DisputeResponse, DisputeResponsePaymentsRetrieve, SubmitEvidenceRequest}; + +impl ApiEventMetric for SubmitEvidenceRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} +impl ApiEventMetric for DisputeResponsePaymentsRetrieve { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} +impl ApiEventMetric for DisputeResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index c2bf50d96c31..e755e0f9c4c6 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -50,6 +50,9 @@ pub enum ApiEventsType { RustLocker, FraudCheck, Recon, + Dispute { + dispute_id: String, + }, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 78a66d2f04e7..3d74a0288404 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -114,7 +114,6 @@ impl_misc_api_event_type!( CreateFileRequest, FileId, AttachEvidenceRequest, - DisputeId, PaymentLinkFormData, ConfigUpdate ); @@ -142,3 +141,11 @@ impl ApiEventMetric for PaymentsRedirectResponseData { }) } } + +impl ApiEventMetric for DisputeId { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Dispute { + dispute_id: self.dispute_id.clone(), + }) + } +} From a9638d118e0b68653fef3bec2ce8aa3c47feedd3 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:16:03 +0530 Subject: [PATCH 406/443] refactor: add support for extending file storage to other schemes and provide a runtime flag for the same (#3348) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 31 ++-- config/config.example.toml | 8 + config/development.toml | 3 + config/docker_compose.toml | 3 + crates/external_services/Cargo.toml | 2 + crates/external_services/src/file_storage.rs | 96 +++++++++++ .../src/file_storage/aws_s3.rs | 158 ++++++++++++++++++ .../src/file_storage/file_system.rs | 144 ++++++++++++++++ crates/external_services/src/lib.rs | 1 + crates/router/Cargo.toml | 8 +- crates/router/src/configs/settings.rs | 21 +-- crates/router/src/configs/validations.rs | 19 --- crates/router/src/core/files.rs | 8 - crates/router/src/core/files/fs_utils.rs | 57 ------- crates/router/src/core/files/helpers.rs | 67 ++------ crates/router/src/core/files/s3_utils.rs | 87 ---------- crates/router/src/routes/app.rs | 5 + docker-compose.yml | 1 + 18 files changed, 461 insertions(+), 258 deletions(-) create mode 100644 crates/external_services/src/file_storage.rs create mode 100644 crates/external_services/src/file_storage/aws_s3.rs create mode 100644 crates/external_services/src/file_storage/file_system.rs delete mode 100644 crates/router/src/core/files/fs_utils.rs delete mode 100644 crates/router/src/core/files/s3_utils.rs diff --git a/Cargo.lock b/Cargo.lock index f920b1ea9c53..b86facad8165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2481,6 +2481,7 @@ dependencies = [ "async-trait", "aws-config", "aws-sdk-kms", + "aws-sdk-s3", "aws-sdk-sesv2", "aws-sdk-sts", "aws-smithy-client", @@ -2726,9 +2727,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -2736,9 +2737,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" @@ -2764,9 +2765,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -2785,9 +2786,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -2796,15 +2797,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" @@ -2814,9 +2815,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -5155,8 +5156,6 @@ dependencies = [ "async-bb8-diesel", "async-trait", "awc", - "aws-config", - "aws-sdk-s3", "base64 0.21.5", "bb8", "bigdecimal", diff --git a/config/config.example.toml b/config/config.example.toml index 0ad50736e9ed..27d1f8b18c5a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -535,3 +535,11 @@ refund_analytics_topic = "topic" # Kafka topic to be used for Refund events api_logs_topic = "topic" # Kafka topic to be used for incoming api events connector_logs_topic = "topic" # Kafka topic to be used for connector api events outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events + +# File storage configuration +[file_storage] +file_storage_backend = "aws_s3" # File storage backend to be used + +[file_storage.aws_s3] +region = "us-east-1" # The AWS region used by the AWS S3 for file storage +bucket_name = "bucket1" # The AWS S3 bucket name for file storage diff --git a/config/development.toml b/config/development.toml index b23f68680e64..5fbe9607cd33 100644 --- a/config/development.toml +++ b/config/development.toml @@ -544,3 +544,6 @@ client_id = "" client_secret = "" partner_id = "" enabled = true + +[file_storage] +file_storage_backend = "file_system" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 8af1528e1771..8dd01a3d1ceb 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -399,3 +399,6 @@ enabled = true [events] source = "logs" + +[file_storage] +file_storage_backend = "file_system" diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 6552b57b0e54..bf836af71a79 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] kms = ["dep:aws-config", "dep:aws-sdk-kms"] email = ["dep:aws-config"] +aws_s3 = ["dep:aws-config", "dep:aws-sdk-s3"] hashicorp-vault = [ "dep:vaultrs" ] [dependencies] @@ -18,6 +19,7 @@ aws-config = { version = "0.55.3", optional = true } aws-sdk-kms = { version = "0.28.0", optional = true } aws-sdk-sesv2 = "0.28.0" aws-sdk-sts = "0.28.0" +aws-sdk-s3 = { version = "0.28.0", optional = true } aws-smithy-client = "0.55.3" base64 = "0.21.2" dyn-clone = "1.0.11" diff --git a/crates/external_services/src/file_storage.rs b/crates/external_services/src/file_storage.rs new file mode 100644 index 000000000000..fb419b6ec643 --- /dev/null +++ b/crates/external_services/src/file_storage.rs @@ -0,0 +1,96 @@ +//! +//! Module for managing file storage operations with support for multiple storage schemes. +//! + +use std::fmt::{Display, Formatter}; + +use common_utils::errors::CustomResult; + +/// Includes functionality for AWS S3 storage operations. +#[cfg(feature = "aws_s3")] +mod aws_s3; + +mod file_system; + +/// Enum representing different file storage configurations, allowing for multiple storage schemes. +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(tag = "file_storage_backend")] +#[serde(rename_all = "snake_case")] +pub enum FileStorageConfig { + /// AWS S3 storage configuration. + #[cfg(feature = "aws_s3")] + AwsS3 { + /// Configuration for AWS S3 file storage. + aws_s3: aws_s3::AwsFileStorageConfig, + }, + /// Local file system storage configuration. + #[default] + FileSystem, +} + +impl FileStorageConfig { + /// Validates the file storage configuration. + pub fn validate(&self) -> Result<(), InvalidFileStorageConfig> { + match self { + #[cfg(feature = "aws_s3")] + Self::AwsS3 { aws_s3 } => aws_s3.validate(), + Self::FileSystem => Ok(()), + } + } + + /// Retrieves the appropriate file storage client based on the file storage configuration. + pub async fn get_file_storage_client(&self) -> Box { + match self { + #[cfg(feature = "aws_s3")] + Self::AwsS3 { aws_s3 } => Box::new(aws_s3::AwsFileStorageClient::new(aws_s3).await), + Self::FileSystem => Box::new(file_system::FileSystem), + } + } +} + +/// Trait for file storage operations +#[async_trait::async_trait] +pub trait FileStorageInterface: dyn_clone::DynClone + Sync + Send { + /// Uploads a file to the selected storage scheme. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileStorageError>; + + /// Deletes a file from the selected storage scheme. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileStorageError>; + + /// Retrieves a file from the selected storage scheme. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileStorageError>; +} + +dyn_clone::clone_trait_object!(FileStorageInterface); + +/// Error thrown when the file storage config is invalid +#[derive(Debug, Clone)] +pub struct InvalidFileStorageConfig(&'static str); + +impl std::error::Error for InvalidFileStorageConfig {} + +impl Display for InvalidFileStorageConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "file_storage: {}", self.0) + } +} + +/// Represents errors that can occur during file storage operations. +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum FileStorageError { + /// Indicates that the file upload operation failed. + #[error("Failed to upload file")] + UploadFailed, + + /// Indicates that the file retrieval operation failed. + #[error("Failed to retrieve file")] + RetrieveFailed, + + /// Indicates that the file deletion operation failed. + #[error("Failed to delete file")] + DeleteFailed, +} diff --git a/crates/external_services/src/file_storage/aws_s3.rs b/crates/external_services/src/file_storage/aws_s3.rs new file mode 100644 index 000000000000..86d1c0f0efab --- /dev/null +++ b/crates/external_services/src/file_storage/aws_s3.rs @@ -0,0 +1,158 @@ +use aws_config::meta::region::RegionProviderChain; +use aws_sdk_s3::{ + operation::{ + delete_object::DeleteObjectError, get_object::GetObjectError, put_object::PutObjectError, + }, + Client, +}; +use aws_sdk_sts::config::Region; +use common_utils::{errors::CustomResult, ext_traits::ConfigExt}; +use error_stack::ResultExt; + +use super::InvalidFileStorageConfig; +use crate::file_storage::{FileStorageError, FileStorageInterface}; + +/// Configuration for AWS S3 file storage. +#[derive(Debug, serde::Deserialize, Clone, Default)] +#[serde(default)] +pub struct AwsFileStorageConfig { + /// The AWS region to send file uploads + region: String, + /// The AWS s3 bucket to send file uploads + bucket_name: String, +} + +impl AwsFileStorageConfig { + /// Validates the AWS S3 file storage configuration. + pub(super) fn validate(&self) -> Result<(), InvalidFileStorageConfig> { + use common_utils::fp_utils::when; + + when(self.region.is_default_or_empty(), || { + Err(InvalidFileStorageConfig("aws s3 region must not be empty")) + })?; + + when(self.bucket_name.is_default_or_empty(), || { + Err(InvalidFileStorageConfig( + "aws s3 bucket name must not be empty", + )) + }) + } +} + +/// AWS S3 file storage client. +#[derive(Debug, Clone)] +pub(super) struct AwsFileStorageClient { + /// AWS S3 client + inner_client: Client, + /// The name of the AWS S3 bucket. + bucket_name: String, +} + +impl AwsFileStorageClient { + /// Creates a new AWS S3 file storage client. + pub(super) async fn new(config: &AwsFileStorageConfig) -> Self { + let region_provider = RegionProviderChain::first_try(Region::new(config.region.clone())); + let sdk_config = aws_config::from_env().region(region_provider).load().await; + Self { + inner_client: Client::new(&sdk_config), + bucket_name: config.bucket_name.clone(), + } + } + + /// Uploads a file to AWS S3. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), AwsS3StorageError> { + self.inner_client + .put_object() + .bucket(&self.bucket_name) + .key(file_key) + .body(file.into()) + .send() + .await + .map_err(AwsS3StorageError::UploadFailure)?; + Ok(()) + } + + /// Deletes a file from AWS S3. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), AwsS3StorageError> { + self.inner_client + .delete_object() + .bucket(&self.bucket_name) + .key(file_key) + .send() + .await + .map_err(AwsS3StorageError::DeleteFailure)?; + Ok(()) + } + + /// Retrieves a file from AWS S3. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, AwsS3StorageError> { + Ok(self + .inner_client + .get_object() + .bucket(&self.bucket_name) + .key(file_key) + .send() + .await + .map_err(AwsS3StorageError::RetrieveFailure)? + .body + .collect() + .await + .map_err(AwsS3StorageError::UnknownError)? + .to_vec()) + } +} + +#[async_trait::async_trait] +impl FileStorageInterface for AwsFileStorageClient { + /// Uploads a file to AWS S3. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileStorageError> { + self.upload_file(file_key, file) + .await + .change_context(FileStorageError::UploadFailed)?; + Ok(()) + } + + /// Deletes a file from AWS S3. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileStorageError> { + self.delete_file(file_key) + .await + .change_context(FileStorageError::DeleteFailed)?; + Ok(()) + } + + /// Retrieves a file from AWS S3. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileStorageError> { + Ok(self + .retrieve_file(file_key) + .await + .change_context(FileStorageError::RetrieveFailed)?) + } +} + +/// Enum representing errors that can occur during AWS S3 file storage operations. +#[derive(Debug, thiserror::Error)] +enum AwsS3StorageError { + /// Error indicating that file upload to S3 failed. + #[error("File upload to S3 failed: {0:?}")] + UploadFailure(aws_smithy_client::SdkError), + + /// Error indicating that file retrieval from S3 failed. + #[error("File retrieve from S3 failed: {0:?}")] + RetrieveFailure(aws_smithy_client::SdkError), + + /// Error indicating that file deletion from S3 failed. + #[error("File delete from S3 failed: {0:?}")] + DeleteFailure(aws_smithy_client::SdkError), + + /// Unknown error occurred. + #[error("Unknown error occurred: {0:?}")] + UnknownError(aws_sdk_s3::primitives::ByteStreamError), +} diff --git a/crates/external_services/src/file_storage/file_system.rs b/crates/external_services/src/file_storage/file_system.rs new file mode 100644 index 000000000000..15ca84deeb8e --- /dev/null +++ b/crates/external_services/src/file_storage/file_system.rs @@ -0,0 +1,144 @@ +//! +//! Module for local file system storage operations +//! + +use std::{ + fs::{remove_file, File}, + io::{Read, Write}, + path::PathBuf, +}; + +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use crate::file_storage::{FileStorageError, FileStorageInterface}; + +/// Constructs the file path for a given file key within the file system. +/// The file path is generated based on the workspace path and the provided file key. +fn get_file_path(file_key: impl AsRef) -> PathBuf { + let mut file_path = PathBuf::new(); + #[cfg(feature = "logs")] + file_path.push(router_env::env::workspace_path()); + #[cfg(not(feature = "logs"))] + file_path.push(std::env::current_dir().unwrap_or(".".into())); + + file_path.push("files"); + file_path.push(file_key.as_ref()); + file_path +} + +/// Represents a file system for storing and managing files locally. +#[derive(Debug, Clone)] +pub(super) struct FileSystem; + +impl FileSystem { + /// Saves the provided file data to the file system under the specified file key. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileSystemStorageError> { + let file_path = get_file_path(file_key); + + // Ignore the file name and create directories in the `file_path` if not exists + std::fs::create_dir_all( + file_path + .parent() + .ok_or(FileSystemStorageError::CreateDirFailed) + .into_report() + .attach_printable("Failed to obtain parent directory")?, + ) + .into_report() + .change_context(FileSystemStorageError::CreateDirFailed)?; + + let mut file_handler = File::create(file_path) + .into_report() + .change_context(FileSystemStorageError::CreateFailure)?; + file_handler + .write_all(&file) + .into_report() + .change_context(FileSystemStorageError::WriteFailure)?; + Ok(()) + } + + /// Deletes the file associated with the specified file key from the file system. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileSystemStorageError> { + let file_path = get_file_path(file_key); + remove_file(file_path) + .into_report() + .change_context(FileSystemStorageError::DeleteFailure)?; + Ok(()) + } + + /// Retrieves the file content associated with the specified file key from the file system. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileSystemStorageError> { + let mut received_data: Vec = Vec::new(); + let file_path = get_file_path(file_key); + let mut file = File::open(file_path) + .into_report() + .change_context(FileSystemStorageError::FileOpenFailure)?; + file.read_to_end(&mut received_data) + .into_report() + .change_context(FileSystemStorageError::ReadFailure)?; + Ok(received_data) + } +} + +#[async_trait::async_trait] +impl FileStorageInterface for FileSystem { + /// Saves the provided file data to the file system under the specified file key. + async fn upload_file( + &self, + file_key: &str, + file: Vec, + ) -> CustomResult<(), FileStorageError> { + self.upload_file(file_key, file) + .await + .change_context(FileStorageError::UploadFailed)?; + Ok(()) + } + + /// Deletes the file associated with the specified file key from the file system. + async fn delete_file(&self, file_key: &str) -> CustomResult<(), FileStorageError> { + self.delete_file(file_key) + .await + .change_context(FileStorageError::DeleteFailed)?; + Ok(()) + } + + /// Retrieves the file content associated with the specified file key from the file system. + async fn retrieve_file(&self, file_key: &str) -> CustomResult, FileStorageError> { + Ok(self + .retrieve_file(file_key) + .await + .change_context(FileStorageError::RetrieveFailed)?) + } +} + +/// Represents an error that can occur during local file system storage operations. +#[derive(Debug, thiserror::Error)] +enum FileSystemStorageError { + /// Error indicating opening a file failed + #[error("Failed while opening the file")] + FileOpenFailure, + + /// Error indicating file creation failed. + #[error("Failed to create file")] + CreateFailure, + + /// Error indicating reading a file failed. + #[error("Failed while reading the file")] + ReadFailure, + + /// Error indicating writing to a file failed. + #[error("Failed while writing into file")] + WriteFailure, + + /// Error indicating file deletion failed. + #[error("Failed while deleting the file")] + DeleteFailure, + + /// Error indicating directory creation failed + #[error("Failed while creating a directory")] + CreateDirFailed, +} diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index 9bf4916eec33..bba65873e91a 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -9,6 +9,7 @@ pub mod email; #[cfg(feature = "kms")] pub mod kms; +pub mod file_storage; #[cfg(feature = "hashicorp-vault")] pub mod hashicorp_vault; diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index e575daf7e7ad..3d129edfe3f4 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -10,10 +10,10 @@ license.workspace = true [features] default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] -aws_s3 = ["dep:aws-sdk-s3", "dep:aws-config"] -kms = ["external_services/kms", "dep:aws-config"] +aws_s3 = ["external_services/aws_s3"] +kms = ["external_services/kms"] hashicorp-vault = ["external_services/hashicorp-vault"] -email = ["external_services/email", "dep:aws-config", "olap"] +email = ["external_services/email", "olap"] frm = [] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "aws_s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] @@ -42,8 +42,6 @@ actix-web = "4.3.1" async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } argon2 = { version = "0.5.0", features = ["std"] } async-trait = "0.1.68" -aws-config = { version = "0.55.3", optional = true } -aws-sdk-s3 = { version = "0.28.0", optional = true } base64 = "0.21.2" bb8 = "0.8" bigdecimal = "0.3.1" diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 3c1d9f7d397e..146a1ace28e6 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -11,6 +11,7 @@ use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; #[cfg(feature = "email")] use external_services::email::EmailSettings; +use external_services::file_storage::FileStorageConfig; #[cfg(feature = "hashicorp-vault")] use external_services::hashicorp_vault; #[cfg(feature = "kms")] @@ -90,10 +91,9 @@ pub struct Settings { pub api_keys: ApiKeys, #[cfg(feature = "kms")] pub kms: kms::KmsConfig, + pub file_storage: FileStorageConfig, #[cfg(feature = "hashicorp-vault")] pub hc_vault: hashicorp_vault::HashiCorpVaultConfig, - #[cfg(feature = "aws_s3")] - pub file_upload_config: FileUploadConfig, pub tokenization: TokenizationConfig, pub connector_customer: ConnectorCustomer, #[cfg(feature = "dummy_connector")] @@ -721,16 +721,6 @@ pub struct ApiKeys { pub expiry_reminder_days: Vec, } -#[cfg(feature = "aws_s3")] -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default)] -pub struct FileUploadConfig { - /// The AWS region to send file uploads - pub region: String, - /// The AWS s3 bucket to send file uploads - pub bucket_name: String, -} - #[derive(Debug, Deserialize, Clone, Default)] pub struct DelayedSessionConfig { #[serde(deserialize_with = "deser_to_get_connectors")] @@ -853,8 +843,11 @@ impl Settings { self.kms .validate() .map_err(|error| ApplicationError::InvalidConfigurationValueError(error.into()))?; - #[cfg(feature = "aws_s3")] - self.file_upload_config.validate()?; + + self.file_storage + .validate() + .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?; + self.lock_settings.validate()?; self.events.validate()?; Ok(()) diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 0b286ece8435..910ae7543479 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -127,25 +127,6 @@ impl super::settings::DrainerSettings { } } -#[cfg(feature = "aws_s3")] -impl super::settings::FileUploadConfig { - pub fn validate(&self) -> Result<(), ApplicationError> { - use common_utils::fp_utils::when; - - when(self.region.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "s3 region must not be empty".into(), - )) - })?; - - when(self.bucket_name.is_default_or_empty(), || { - Err(ApplicationError::InvalidConfigurationValueError( - "s3 bucket name must not be empty".into(), - )) - }) - } -} - impl super::settings::ApiKeys { pub fn validate(&self) -> Result<(), ApplicationError> { use common_utils::fp_utils::when; diff --git a/crates/router/src/core/files.rs b/crates/router/src/core/files.rs index f3e564898061..d3f490a0a6ce 100644 --- a/crates/router/src/core/files.rs +++ b/crates/router/src/core/files.rs @@ -1,9 +1,4 @@ pub mod helpers; -#[cfg(feature = "aws_s3")] -pub mod s3_utils; - -#[cfg(not(feature = "aws_s3"))] -pub mod fs_utils; use api_models::files; use error_stack::{IntoReport, ResultExt}; @@ -29,10 +24,7 @@ pub async fn files_create_core( ) .await?; let file_id = common_utils::generate_id(consts::ID_LENGTH, "file"); - #[cfg(feature = "aws_s3")] let file_key = format!("{}/{}", merchant_account.merchant_id, file_id); - #[cfg(not(feature = "aws_s3"))] - let file_key = format!("{}_{}", merchant_account.merchant_id, file_id); let file_new = diesel_models::file::FileMetadataNew { file_id: file_id.clone(), merchant_id: merchant_account.merchant_id.clone(), diff --git a/crates/router/src/core/files/fs_utils.rs b/crates/router/src/core/files/fs_utils.rs deleted file mode 100644 index 795f2fad7591..000000000000 --- a/crates/router/src/core/files/fs_utils.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::{ - fs::{remove_file, File}, - io::{Read, Write}, - path::PathBuf, -}; - -use common_utils::errors::CustomResult; -use error_stack::{IntoReport, ResultExt}; - -use crate::{core::errors, env}; - -pub fn get_file_path(file_key: String) -> PathBuf { - let mut file_path = PathBuf::new(); - file_path.push(env::workspace_path()); - file_path.push("files"); - file_path.push(file_key); - file_path -} - -pub fn save_file_to_fs( - file_key: String, - file_data: Vec, -) -> CustomResult<(), errors::ApiErrorResponse> { - let file_path = get_file_path(file_key); - let mut file = File::create(file_path) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to create file")?; - file.write_all(&file_data) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while writing into file")?; - Ok(()) -} - -pub fn delete_file_from_fs(file_key: String) -> CustomResult<(), errors::ApiErrorResponse> { - let file_path = get_file_path(file_key); - remove_file(file_path) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while deleting the file")?; - Ok(()) -} - -pub fn retrieve_file_from_fs(file_key: String) -> CustomResult, errors::ApiErrorResponse> { - let mut received_data: Vec = Vec::new(); - let file_path = get_file_path(file_key); - let mut file = File::open(file_path) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while opening the file")?; - file.read_to_end(&mut received_data) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while reading the file")?; - Ok(received_data) -} diff --git a/crates/router/src/core/files/helpers.rs b/crates/router/src/core/files/helpers.rs index 9205d42aeee7..0a509b238afa 100644 --- a/crates/router/src/core/files/helpers.rs +++ b/crates/router/src/core/files/helpers.rs @@ -6,7 +6,7 @@ use futures::TryStreamExt; use crate::{ core::{ errors::{self, StorageErrorExt}, - files, payments, utils, + payments, utils, }, routes::AppState, services, @@ -30,37 +30,6 @@ pub async fn get_file_purpose(field: &mut Field) -> Option { } } -pub async fn upload_file( - #[cfg(feature = "aws_s3")] state: &AppState, - file_key: String, - file: Vec, -) -> CustomResult<(), errors::ApiErrorResponse> { - #[cfg(feature = "aws_s3")] - return files::s3_utils::upload_file_to_s3(state, file_key, file).await; - #[cfg(not(feature = "aws_s3"))] - return files::fs_utils::save_file_to_fs(file_key, file); -} - -pub async fn delete_file( - #[cfg(feature = "aws_s3")] state: &AppState, - file_key: String, -) -> CustomResult<(), errors::ApiErrorResponse> { - #[cfg(feature = "aws_s3")] - return files::s3_utils::delete_file_from_s3(state, file_key).await; - #[cfg(not(feature = "aws_s3"))] - return files::fs_utils::delete_file_from_fs(file_key); -} - -pub async fn retrieve_file( - #[cfg(feature = "aws_s3")] state: &AppState, - file_key: String, -) -> CustomResult, errors::ApiErrorResponse> { - #[cfg(feature = "aws_s3")] - return files::s3_utils::retrieve_file_from_s3(state, file_key).await; - #[cfg(not(feature = "aws_s3"))] - return files::fs_utils::retrieve_file_from_fs(file_key); -} - pub async fn validate_file_upload( state: &AppState, merchant_account: domain::MerchantAccount, @@ -132,14 +101,11 @@ pub async fn delete_file_using_file_id( .attach_printable("File not available")?, }; match provider { - diesel_models::enums::FileUploadProvider::Router => { - delete_file( - #[cfg(feature = "aws_s3")] - state, - provider_file_id, - ) + diesel_models::enums::FileUploadProvider::Router => state + .file_storage_client + .delete_file(&provider_file_id) .await - } + .change_context(errors::ApiErrorResponse::InternalServerError), _ => Err(errors::ApiErrorResponse::FileProviderNotSupported { message: "Not Supported because provider is not Router".to_string(), } @@ -234,12 +200,11 @@ pub async fn retrieve_file_and_provider_file_id_from_file_id( match provider { diesel_models::enums::FileUploadProvider::Router => Ok(( Some( - retrieve_file( - #[cfg(feature = "aws_s3")] - state, - provider_file_id.clone(), - ) - .await?, + state + .file_storage_client + .retrieve_file(&provider_file_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?, ), Some(provider_file_id), )), @@ -364,13 +329,11 @@ pub async fn upload_and_get_provider_provider_file_id_profile_id( payment_attempt.merchant_connector_id, )) } else { - upload_file( - #[cfg(feature = "aws_s3")] - state, - file_key.clone(), - create_file_request.file.clone(), - ) - .await?; + state + .file_storage_client + .upload_file(&file_key, create_file_request.file.clone()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; Ok(( file_key, api_models::enums::FileUploadProvider::Router, diff --git a/crates/router/src/core/files/s3_utils.rs b/crates/router/src/core/files/s3_utils.rs deleted file mode 100644 index 228c23528cd5..000000000000 --- a/crates/router/src/core/files/s3_utils.rs +++ /dev/null @@ -1,87 +0,0 @@ -use aws_config::{self, meta::region::RegionProviderChain}; -use aws_sdk_s3::{config::Region, Client}; -use common_utils::errors::CustomResult; -use error_stack::{IntoReport, ResultExt}; -use futures::TryStreamExt; - -use crate::{core::errors, routes}; - -async fn get_aws_client(state: &routes::AppState) -> Client { - let region_provider = - RegionProviderChain::first_try(Region::new(state.conf.file_upload_config.region.clone())); - let sdk_config = aws_config::from_env().region(region_provider).load().await; - Client::new(&sdk_config) -} - -pub async fn upload_file_to_s3( - state: &routes::AppState, - file_key: String, - file: Vec, -) -> CustomResult<(), errors::ApiErrorResponse> { - let client = get_aws_client(state).await; - let bucket_name = &state.conf.file_upload_config.bucket_name; - // Upload file to S3 - let upload_res = client - .put_object() - .bucket(bucket_name) - .key(file_key.clone()) - .body(file.into()) - .send() - .await; - upload_res - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("File upload to S3 failed")?; - Ok(()) -} - -pub async fn delete_file_from_s3( - state: &routes::AppState, - file_key: String, -) -> CustomResult<(), errors::ApiErrorResponse> { - let client = get_aws_client(state).await; - let bucket_name = &state.conf.file_upload_config.bucket_name; - // Delete file from S3 - let delete_res = client - .delete_object() - .bucket(bucket_name) - .key(file_key) - .send() - .await; - delete_res - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("File delete from S3 failed")?; - Ok(()) -} - -pub async fn retrieve_file_from_s3( - state: &routes::AppState, - file_key: String, -) -> CustomResult, errors::ApiErrorResponse> { - let client = get_aws_client(state).await; - let bucket_name = &state.conf.file_upload_config.bucket_name; - // Get file data from S3 - let get_res = client - .get_object() - .bucket(bucket_name) - .key(file_key) - .send() - .await; - let mut object = get_res - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("File retrieve from S3 failed")?; - let mut received_data: Vec = Vec::new(); - while let Some(bytes) = object - .body - .try_next() - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid file data received from S3")? - { - received_data.extend_from_slice(&bytes); // Collect the bytes in the Vec - } - Ok(received_data) -} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index e18e4d85c704..ae0328c56f6a 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -5,6 +5,7 @@ use actix_web::{web, Scope}; use analytics::AnalyticsConfig; #[cfg(feature = "email")] use external_services::email::{ses::AwsSes, EmailService}; +use external_services::file_storage::FileStorageInterface; #[cfg(all(feature = "olap", feature = "hashicorp-vault"))] use external_services::hashicorp_vault::decrypt::VaultFetch; #[cfg(feature = "kms")] @@ -68,6 +69,7 @@ pub struct AppState { #[cfg(feature = "olap")] pub pool: crate::analytics::AnalyticsProvider, pub request_id: Option, + pub file_storage_client: Box, } impl scheduler::SchedulerAppState for AppState { @@ -266,6 +268,8 @@ impl AppState { #[cfg(feature = "email")] let email_client = Arc::new(create_email_client(&conf).await); + let file_storage_client = conf.file_storage.get_file_storage_client().await; + Self { flow_name: String::from("default"), store, @@ -279,6 +283,7 @@ impl AppState { #[cfg(feature = "olap")] pool, request_id: None, + file_storage_client, } }) .await diff --git a/docker-compose.yml b/docker-compose.yml index 3839269a5220..e55008f1e34e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,6 +54,7 @@ services: - router_net volumes: - ./config:/local/config + - ./files:/local/bin/files labels: logs: "promtail" healthcheck: From 87191d687cd66bf096bfb98ffe51a805b4b76a03 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:25:59 +0530 Subject: [PATCH 407/443] refactor(settings): make the function to deserialize hashsets more generic (#3104) --- Cargo.lock | 19 +- config/config.example.toml | 4 +- config/deployments/integration_test.toml | 26 +- config/deployments/production.toml | 44 ++-- config/deployments/sandbox.toml | 42 ++-- config/development.toml | 24 +- config/docker_compose.toml | 4 +- crates/common_enums/src/enums.rs | 105 +++++++- crates/currency_conversion/Cargo.toml | 2 +- crates/currency_conversion/src/types.rs | 25 ++ crates/kgraph_utils/src/transformers.rs | 25 ++ crates/router/src/configs/settings.rs | 296 +++++++++++------------ openapi/openapi_spec.json | 27 ++- 13 files changed, 391 insertions(+), 252 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b86facad8165..a1aabc89a040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2605,9 +2605,9 @@ dependencies = [ [[package]] name = "fred" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e8094c30c33132e948eb7e1b740cfdaa5a6702610bd3a2744002ec3575cd68" +checksum = "9282e65613822eea90c99872c51afa1de61542215cb11f91456a93f50a5a131a" dependencies = [ "arc-swap", "async-trait", @@ -2628,6 +2628,7 @@ dependencies = [ "tracing", "tracing-futures", "url", + "urlencoding", ] [[package]] @@ -5314,16 +5315,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "rust_decimal_macros" -version = "1.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e43721f4ef7060ebc2c3ede757733209564ca8207f47674181bcd425dd76945" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -5486,11 +5477,9 @@ dependencies = [ [[package]] name = "rusty-money" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +source = "git+https://github.com/varunsrin/rusty_money?rev=bbc0150742a0fff905225ff11ee09388e9babdcc#bbc0150742a0fff905225ff11ee09388e9babdcc" dependencies = [ "rust_decimal", - "rust_decimal_macros", ] [[package]] diff --git a/config/config.example.toml b/config/config.example.toml index 27d1f8b18c5a..d53a6e28ef6f 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -415,7 +415,7 @@ alfamart = { country = "ID", currency = "IDR" } indomaret = { country = "ID", currency = "IDR" } open_banking_uk = { country = "GB", currency = "GBP" } oxxo = { country = "MX", currency = "MXN" } -pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } seven_eleven = { country = "JP", currency = "JPY" } lawson = { country = "JP", currency = "JPY" } mini_stop = { country = "JP", currency = "JPY" } @@ -452,7 +452,7 @@ connector_list = "gocardless,stax,stripe" payout_connector_list = "wise" [bank_config.online_banking_fpx] -adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 4a858588b504..d377b3359c94 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -4,7 +4,7 @@ eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" -online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" online_banking_poland.adyen.banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" @@ -133,20 +133,20 @@ giropay = { country = "DE", currency = "EUR" } google_pay.country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" ideal = { country = "NL", currency = "EUR" } klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US,CA", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } -paypal.country = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" +paypal.currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } [pm_filters.adyen] ach = { country = "US", currency = "USD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } alfamart = { country = "ID", currency = "IDR" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } ali_pay_hk = { country = "HK", currency = "HKD" } alma = { country = "FR", currency = "EUR" } -apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,UK,SE,NO,AK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } atome = { country = "MY,SG", currency = "MYR,SGD" } -bacs = { country = "UK", currency = "GBP" } +bacs = { country = "GB", currency = "GBP" } bancontact_card = { country = "BE", currency = "EUR" } bca_bank_transfer = { country = "ID", currency = "IDR" } bizum = { country = "ES", currency = "EUR" } @@ -162,11 +162,11 @@ family_mart = { country = "JP", currency = "JPY" } gcash = { country = "PH", currency = "PHP" } giropay = { country = "DE", currency = "EUR" } go_pay = { country = "ID", currency = "IDR" } -google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } ideal = { country = "NL", currency = "EUR" } indomaret = { country = "ID", currency = "IDR" } kakao_pay = { country = "KR", currency = "KRW" } -klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } lawson = { country = "JP", currency = "JPY" } mandiri_va = { country = "ID", currency = "IDR" } mb_way = { country = "PT", currency = "EUR" } @@ -184,20 +184,20 @@ open_banking_uk = { country = "GB", currency = "GBP" } oxxo = { country = "MX", currency = "MXN" } pay_bright = { country = "CA", currency = "CAD" } pay_easy = { country = "JP", currency = "JPY" } -pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } permata_bank_transfer = { country = "ID", currency = "IDR" } seicomart = { country = "JP", currency = "JPY" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } seven_eleven = { country = "JP", currency = "JPY" } -sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } -trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } twint = { country = "CH", currency = "CHF" } vipps = { country = "NO", currency = "NOK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD,CNY" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD,CNY" } [pm_filters.authorizedotnet] google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 376ae579a507..d4671d3a99d2 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -4,9 +4,9 @@ eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" -online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" online_banking_poland.adyen.banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" -online_banking_slovakia.adyen.banks = "e_platby_v_u_b,e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo,volksbank_gruppe,volkskredit_bank_ag,vr_bank_braunau" +online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" open_banking_uk.adyen.banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled" przelewy24.stripe.banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,banki_spbdzielcze,blik,bnp_paribas,boz,citi,credit_agricole,e_transfer_pocztowy24,getin_bank,idea_bank,inteligo,mbank_mtransfer,nest_przelew,noble_pay,pbac_z_ipko,plus_bank,santander_przelew24,toyota_bank,volkswagen_bank" @@ -127,17 +127,17 @@ payout_eligibility = true [pm_filters.default] ach = { country = "US", currency = "USD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD,CNY" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD,CNY" } apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } -bacs = { country = "UK", currency = "GBP" } +bacs = { country = "GB", currency = "GBP" } bancontact_card = { country = "BE", currency = "EUR" } blik = { country = "PL", currency = "PLN" } eps = { country = "AT", currency = "EUR" } giropay = { country = "DE", currency = "EUR" } -google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } ideal = { country = "NL", currency = "EUR" } -klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } mb_way = { country = "PT", currency = "EUR" } mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } @@ -145,24 +145,24 @@ online_banking_finland = { country = "FI", currency = "EUR" } online_banking_poland = { country = "PL", currency = "PLN" } online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } pay_bright = { country = "CA", currency = "CAD" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } -sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } -trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } [pm_filters.adyen] ach = { country = "US", currency = "USD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,CA,ES,FR,IT,NZ,UK,US", currency = "USD,AUD,CAD,NZD,GBP" } +afterpay_clearpay = { country = "AU,CA,ES,FR,IT,NZ,GB,US", currency = "USD,AUD,CAD,NZD,GBP" } alfamart = { country = "ID", currency = "IDR" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } ali_pay_hk = { country = "HK", currency = "HKD" } alma = { country = "FR", currency = "EUR" } -apple_pay = { country = "AE,AK,AM,AR,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CN,CO,CR,CY,CZ,DE,DK,EE,ES,FI,FO,FR,GB,GE,GG,GL,GR,HK,HR,HU,IE,IL,IM,IS,IT,JE,JO,JP,KW,KZ,LI,LT,LU,LV,MC,MD,ME,MO,MT,MX,MY,NL,NO,NZ,PE,PL,PS,PT,QA,RO,RS,SA,SE,SG,SI,SK,SM,TW,UA,UK,UM,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +apple_pay = { country = "AE,AM,AR,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CN,CO,CR,CY,CZ,DE,DK,EE,ES,FI,FO,FR,GB,GE,GG,GL,GR,HK,HR,HU,IE,IL,IM,IS,IT,JE,JO,JP,KW,KZ,LI,LT,LU,LV,MC,MD,ME,MO,MT,MX,MY,NL,NO,NZ,PE,PL,PS,PT,QA,RO,RS,SA,SE,SG,SI,SK,SM,TW,UA,GB,UM,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } atome = { country = "MY,SG", currency = "MYR,SGD" } -bacs = { country = "UK", currency = "GBP" } +bacs = { country = "GB", currency = "GBP" } bancontact_card = { country = "BE", currency = "EUR" } bca_bank_transfer = { country = "ID", currency = "IDR" } bizum = { country = "ES", currency = "EUR" } @@ -178,11 +178,11 @@ family_mart = { country = "JP", currency = "JPY" } gcash = { country = "PH", currency = "PHP" } giropay = { country = "DE", currency = "EUR" } go_pay = { country = "ID", currency = "IDR" } -google_pay = { country = "AE,AG,AL,AO,AR,AS,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CL,CO,CY,CZ,DE,DK,DO,DZ,EE,EG,ES,FI,FR,GB,GR,HK,HR,HU,ID,IE,IL,IN,IS,IT,JO,JP,KE,KW,KZ,LB,LI,LK,LT,LU,LV,MT,MX,MY,NL,NO,NZ,OM,PA,PE,PH,PK,PL,PT,QA,RO,RU,SA,SE,SG,SI,SK,TH,TR,TW,UA,UK,US,UY,VN,ZA", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +google_pay = { country = "AE,AG,AL,AO,AR,AS,AT,AU,AZ,BE,BG,BH,BR,BY,CA,CH,CL,CO,CY,CZ,DE,DK,DO,DZ,EE,EG,ES,FI,FR,GB,GR,HK,HR,HU,ID,IE,IL,IN,IS,IT,JO,JP,KE,KW,KZ,LB,LI,LK,LT,LU,LV,MT,MX,MY,NL,NO,NZ,OM,PA,PE,PH,PK,PL,PT,QA,RO,RU,SA,SE,SG,SI,SK,TH,TR,TW,UA,GB,US,UY,VN,ZA", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } ideal = { country = "NL", currency = "EUR" } indomaret = { country = "ID", currency = "IDR" } kakao_pay = { country = "KR", currency = "KRW" } -klarna = { country = "AT,BE,CA,CH,DE,DK,ES,FI,FR,GB,IE,IT,NL,NO,PL,PT,SE,UK,US", currency = "AUD,CAD,CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD" } +klarna = { country = "AT,BE,CA,CH,DE,DK,ES,FI,FR,GB,IE,IT,NL,NO,PL,PT,SE,GB,US", currency = "AUD,CAD,CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD" } lawson = { country = "JP", currency = "JPY" } mandiri_va = { country = "ID", currency = "IDR" } mb_way = { country = "PT", currency = "EUR" } @@ -200,20 +200,20 @@ open_banking_uk = { country = "GB", currency = "GBP" } oxxo = { country = "MX", currency = "MXN" } pay_bright = { country = "CA", currency = "CAD" } pay_easy = { country = "JP", currency = "JPY" } -pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } permata_bank_transfer = { country = "ID", currency = "IDR" } seicomart = { country = "JP", currency = "JPY" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } seven_eleven = { country = "JP", currency = "JPY" } -sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE,UK", currency = "EUR" } +sofort = { country = "AT,BE,CH,DE,ES,FI,FR,GB,IT,NL,PL,SE,GB", currency = "EUR" } swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } -trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } twint = { country = "CH", currency = "CHF" } vipps = { country = "NO", currency = "NOK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } [pm_filters.authorizedotnet] google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 01616f3ecd08..abacd3ba5a16 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -4,7 +4,7 @@ eps.stripe.banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria ideal.adyen.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" ideal.stripe.banks = "abn_amro,asn_bank,bunq,handelsbanken,ing,knab,moneyou,rabobank,regiobank,revolut,sns_bank,triodos_bank,van_lanschot" online_banking_czech_republic.adyen.banks = "ceska_sporitelna,komercni_banka,platnosc_online_karta_platnicza" -online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +online_banking_fpx.adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" online_banking_poland.adyen.banks = "blik_psp,place_zipko,m_bank,pay_with_ing,santander_przelew24,bank_pekaosa,bank_millennium,pay_with_alior_bank,banki_spoldzielcze,pay_with_inteligo,bnp_paribas_poland,bank_nowy_sa,credit_agricole,pay_with_bos,pay_with_citi_handlowy,pay_with_plus_bank,toyota_bank,velo_bank,e_transfer_pocztowy24" online_banking_slovakia.adyen.banks = "e_platby_vub,postova_banka,sporo_pay,tatra_pay,viamo" online_banking_thailand.adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" @@ -127,17 +127,17 @@ payout_eligibility = true [pm_filters.default] ach = { country = "US", currency = "USD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } -bacs = { country = "UK", currency = "GBP" } +bacs = { country = "GB", currency = "GBP" } bancontact_card = { country = "BE", currency = "EUR" } blik = { country = "PL", currency = "PLN" } eps = { country = "AT", currency = "EUR" } giropay = { country = "DE", currency = "EUR" } -google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } ideal = { country = "NL", currency = "EUR" } -klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } mb_way = { country = "PT", currency = "EUR" } mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } online_banking_czech_republic = { country = "CZ", currency = "EUR,CZK" } @@ -145,24 +145,24 @@ online_banking_finland = { country = "FI", currency = "EUR" } online_banking_poland = { country = "PL", currency = "PLN" } online_banking_slovakia = { country = "SK", currency = "EUR,CZK" } pay_bright = { country = "CA", currency = "CAD" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } -sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } -trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } [pm_filters.adyen] ach = { country = "US", currency = "USD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } alfamart = { country = "ID", currency = "IDR" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } ali_pay_hk = { country = "HK", currency = "HKD" } alma = { country = "FR", currency = "EUR" } -apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,UK,SE,NO,AK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } atome = { country = "MY,SG", currency = "MYR,SGD" } -bacs = { country = "UK", currency = "GBP" } +bacs = { country = "GB", currency = "GBP" } bancontact_card = { country = "BE", currency = "EUR" } bca_bank_transfer = { country = "ID", currency = "IDR" } bizum = { country = "ES", currency = "EUR" } @@ -178,11 +178,11 @@ family_mart = { country = "JP", currency = "JPY" } gcash = { country = "PH", currency = "PHP" } giropay = { country = "DE", currency = "EUR" } go_pay = { country = "ID", currency = "IDR" } -google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } ideal = { country = "NL", currency = "EUR" } indomaret = { country = "ID", currency = "IDR" } kakao_pay = { country = "KR", currency = "KRW" } -klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } lawson = { country = "JP", currency = "JPY" } mandiri_va = { country = "ID", currency = "IDR" } mb_way = { country = "PT", currency = "EUR" } @@ -200,20 +200,20 @@ open_banking_uk = { country = "GB", currency = "GBP" } oxxo = { country = "MX", currency = "MXN" } pay_bright = { country = "CA", currency = "CAD" } pay_easy = { country = "JP", currency = "JPY" } -pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +pay_safe_card = { country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } permata_bank_transfer = { country = "ID", currency = "IDR" } seicomart = { country = "JP", currency = "JPY" } sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR" } seven_eleven = { country = "JP", currency = "JPY" } -sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } swish = { country = "SE", currency = "SEK" } touch_n_go = { country = "MY", currency = "MYR" } -trustly = { country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } +trustly = { country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK" } twint = { country = "CH", currency = "CHF" } vipps = { country = "NO", currency = "NOK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } [pm_filters.authorizedotnet] google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" diff --git a/config/development.toml b/config/development.toml index 5fbe9607cd33..7c494ea28521 100644 --- a/config/development.toml +++ b/config/development.toml @@ -263,7 +263,7 @@ stripe = { banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,ba adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,halifax,lloyds,monzo,nat_west,nationwide_bank,royal_bank_of_scotland,starling,tsb_bank,tesco_bank,ulster_bank,barclays,hsbc_bank,revolut,santander_przelew24,open_bank_success,open_bank_failure,open_bank_cancelled"} [bank_config.online_banking_fpx] -adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" @@ -293,31 +293,31 @@ ideal = { country = "NL", currency = "EUR" } cashapp = { country = "US", currency = "USD" } [pm_filters.adyen] -google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VEF,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } -apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,UK,SE,NO,AK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } -paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,UK,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +google_pay = { country = "AU,NZ,JP,HK,SG,MY,TH,VN,BH,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,RO,HR,LI,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,TR,IS,CA,US", currency = "AED,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HTG,HUF,IDR,ILS,INR,IQD,JMD,JOD,JPY,KES,KGS,KHR,KMF,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LYD,MAD,MDL,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLE,SOS,SRD,STN,SVC,SZL,THB,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW" } +apple_pay = { country = "AU,NZ,CN,JP,HK,SG,MY,BH,AE,KW,BR,ES,GB,SE,NO,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,LI,UA,MT,SI,GR,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +paypal = { country = "AU,NZ,CN,JP,HK,MY,TH,KR,PH,ID,AE,KW,BR,ES,GB,SE,NO,SK,AT,NL,DE,HU,CY,LU,CH,BE,FR,DK,FI,RO,HR,UA,MT,SI,GI,PT,IE,CZ,EE,LT,LV,IT,PL,IS,CA,US", currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } mobile_pay = { country = "DK,FI", currency = "DKK,SEK,NOK,EUR" } -ali_pay = { country = "AU,N,JP,HK,SG,MY,TH,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } -we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,UK,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +ali_pay = { country = "AU,JP,HK,SG,MY,TH,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,FI,RO,MT,SI,GR,PT,IE,IT,CA,US", currency = "USD,EUR,GBP,JPY,AUD,SGD,CHF,SEK,NOK,NZD,THB,HKD,CAD" } +we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } mb_way = { country = "PT", currency = "EUR" } -klarna = { country = "AT,ES,UK,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +klarna = { country = "AT,ES,GB,SE,NO,AT,NL,DE,CH,BE,FR,DK,FI,PT,IE,IT,PL,CA,US", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } affirm = { country = "US", currency = "USD" } -afterpay_clearpay = { country = "AU,NZ,ES,UK,FR,IT,CA,US", currency = "GBP" } +afterpay_clearpay = { country = "AU,NZ,ES,GB,FR,IT,CA,US", currency = "GBP" } pay_bright = { country = "CA", currency = "CAD" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } giropay = { country = "DE", currency = "EUR" } eps = { country = "AT", currency = "EUR" } -sofort = { country = "ES,UK,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } ideal = { country = "NL", currency = "EUR" } blik = {country = "PL", currency = "PLN"} -trustly = {country = "ES,UK,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} +trustly = {country = "ES,GB,SE,NO,AT,NL,DE,DK,FI,EE,LT,LV", currency = "CZK,DKK,EUR,GBP,NOK,SEK"} online_banking_czech_republic = {country = "CZ", currency = "EUR,CZK"} online_banking_finland = {country = "FI", currency = "EUR"} online_banking_poland = {country = "PL", currency = "PLN"} online_banking_slovakia = {country = "SK", currency = "EUR,CZK"} bancontact_card = {country = "BE", currency = "EUR"} ach = {country = "US", currency = "USD"} -bacs = {country = "UK", currency = "GBP"} +bacs = {country = "GB", currency = "GBP"} sepa = {country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT", currency = "EUR"} ali_pay_hk = {country = "HK", currency = "HKD"} bizum = {country = "ES", currency = "EUR"} @@ -341,7 +341,7 @@ alfamart = {country = "ID", currency = "IDR"} indomaret = {country = "ID", currency = "IDR"} open_banking_uk = {country = "GB", currency = "GBP"} oxxo = {country = "MX", currency = "MXN"} -pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} +pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} seven_eleven = {country = "JP", currency = "JPY"} lawson = {country = "JP", currency = "JPY"} mini_stop = {country = "JP", currency = "JPY"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 8dd01a3d1ceb..79039e136792 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -286,7 +286,7 @@ alfamart = {country = "ID", currency = "IDR"} indomaret = {country = "ID", currency = "IDR"} open_banking_uk = {country = "GB", currency = "GBP"} oxxo = {country = "MX", currency = "MXN"} -pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,UAE,UK,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,ISK,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} +pay_safe_card = {country = "AT,AU,BE,BR,BE,CA,HR,CY,CZ,DK,FI,FR,GE,DE,GI,HU,IS,IE,KW,LV,IE,LI,LT,LU,MT,MX,MD,ME,NL,NZ,NO,PY,PE,PL,PT,RO,SA,RS,SK,SI,ES,SE,CH,TR,AE,GB,US,UY", currency = "EUR,AUD,BRL,CAD,CZK,DKK,GEL,GIP,HUF,KWD,CHF,MXN,MDL,NZD,NOK,PYG,PEN,PLN,RON,SAR,RSD,SEK,TRY,AED,GBP,USD,UYU"} seven_eleven = {country = "JP", currency = "JPY"} lawson = {country = "JP", currency = "JPY"} mini_stop = {country = "JP", currency = "JPY"} @@ -322,7 +322,7 @@ debit = { currency = "USD" } ach = { currency = "USD" } [bank_config.online_banking_fpx] -adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,may_bank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" +adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" [bank_config.online_banking_thailand] adyen.banks = "bangkok_bank,krungsri_bank,krung_thai_bank,the_siam_commercial_bank,kasikorn_bank" diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index c0a363042ec3..7eb65f18a329 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -391,12 +391,15 @@ pub enum Currency { ALL, AMD, ANG, + AOA, ARS, AUD, AWG, AZN, + BAM, BBD, BDT, + BGN, BHD, BIF, BMD, @@ -405,6 +408,7 @@ pub enum Currency { BRL, BSD, BWP, + BYN, BZD, CAD, CHF, @@ -413,6 +417,7 @@ pub enum Currency { COP, CRC, CUP, + CVE, CZK, DJF, DKK, @@ -422,7 +427,9 @@ pub enum Currency { ETB, EUR, FJD, + FKP, GBP, + GEL, GHS, GIP, GMD, @@ -437,6 +444,7 @@ pub enum Currency { IDR, ILS, INR, + IQD, JMD, JOD, JPY, @@ -453,6 +461,7 @@ pub enum Currency { LKR, LRD, LSL, + LYD, MAD, MDL, MGA, @@ -460,11 +469,13 @@ pub enum Currency { MMK, MNT, MOP, + MRU, MUR, MVR, MWK, MXN, MYR, + MZN, NAD, NGN, NIO, @@ -472,6 +483,7 @@ pub enum Currency { NPR, NZD, OMR, + PAB, PEN, PGK, PHP, @@ -480,34 +492,47 @@ pub enum Currency { PYG, QAR, RON, + RSD, RUB, RWF, SAR, + SBD, SCR, SEK, SGD, + SHP, + SLE, SLL, SOS, + SRD, SSP, + STN, SVC, SZL, THB, + TND, + TOP, TRY, TTD, TWD, TZS, + UAH, UGX, #[default] USD, UYU, UZS, + VES, VND, VUV, + WST, XAF, + XCD, XOF, XPF, YER, ZAR, + ZMW, } impl Currency { @@ -564,12 +589,15 @@ impl Currency { Self::ALL => "008", Self::AMD => "051", Self::ANG => "532", + Self::AOA => "973", Self::ARS => "032", Self::AUD => "036", Self::AWG => "533", Self::AZN => "944", + Self::BAM => "977", Self::BBD => "052", Self::BDT => "050", + Self::BGN => "975", Self::BHD => "048", Self::BIF => "108", Self::BMD => "060", @@ -578,6 +606,7 @@ impl Currency { Self::BRL => "986", Self::BSD => "044", Self::BWP => "072", + Self::BYN => "933", Self::BZD => "084", Self::CAD => "124", Self::CHF => "756", @@ -585,6 +614,7 @@ impl Currency { Self::COP => "170", Self::CRC => "188", Self::CUP => "192", + Self::CVE => "132", Self::CZK => "203", Self::DJF => "262", Self::DKK => "208", @@ -594,7 +624,9 @@ impl Currency { Self::ETB => "230", Self::EUR => "978", Self::FJD => "242", + Self::FKP => "238", Self::GBP => "826", + Self::GEL => "981", Self::GHS => "936", Self::GIP => "292", Self::GMD => "270", @@ -609,6 +641,7 @@ impl Currency { Self::IDR => "360", Self::ILS => "376", Self::INR => "356", + Self::IQD => "368", Self::JMD => "388", Self::JOD => "400", Self::JPY => "392", @@ -625,6 +658,7 @@ impl Currency { Self::LKR => "144", Self::LRD => "430", Self::LSL => "426", + Self::LYD => "434", Self::MAD => "504", Self::MDL => "498", Self::MGA => "969", @@ -632,11 +666,13 @@ impl Currency { Self::MMK => "104", Self::MNT => "496", Self::MOP => "446", + Self::MRU => "929", Self::MUR => "480", Self::MVR => "462", Self::MWK => "454", Self::MXN => "484", Self::MYR => "458", + Self::MZN => "943", Self::NAD => "516", Self::NGN => "566", Self::NIO => "558", @@ -644,6 +680,7 @@ impl Currency { Self::NPR => "524", Self::NZD => "554", Self::OMR => "512", + Self::PAB => "590", Self::PEN => "604", Self::PGK => "598", Self::PHP => "608", @@ -653,33 +690,46 @@ impl Currency { Self::QAR => "634", Self::RON => "946", Self::CNY => "156", + Self::RSD => "941", Self::RUB => "643", Self::RWF => "646", Self::SAR => "682", + Self::SBD => "090", Self::SCR => "690", Self::SEK => "752", Self::SGD => "702", + Self::SHP => "654", + Self::SLE => "925", Self::SLL => "694", Self::SOS => "706", + Self::SRD => "968", Self::SSP => "728", + Self::STN => "930", Self::SVC => "222", Self::SZL => "748", Self::THB => "764", + Self::TND => "788", + Self::TOP => "776", Self::TRY => "949", Self::TTD => "780", Self::TWD => "901", Self::TZS => "834", + Self::UAH => "980", Self::UGX => "800", Self::USD => "840", Self::UYU => "858", Self::UZS => "860", + Self::VES => "928", Self::VND => "704", Self::VUV => "548", + Self::WST => "882", Self::XAF => "950", + Self::XCD => "951", Self::XOF => "952", Self::XPF => "953", Self::YER => "886", Self::ZAR => "710", + Self::ZMW => "967", } } @@ -705,12 +755,15 @@ impl Currency { | Self::ALL | Self::AMD | Self::ANG + | Self::AOA | Self::ARS | Self::AUD | Self::AWG | Self::AZN + | Self::BAM | Self::BBD | Self::BDT + | Self::BGN | Self::BHD | Self::BMD | Self::BND @@ -718,6 +771,7 @@ impl Currency { | Self::BRL | Self::BSD | Self::BWP + | Self::BYN | Self::BZD | Self::CAD | Self::CHF @@ -725,6 +779,7 @@ impl Currency { | Self::COP | Self::CRC | Self::CUP + | Self::CVE | Self::CZK | Self::DKK | Self::DOP @@ -733,7 +788,9 @@ impl Currency { | Self::ETB | Self::EUR | Self::FJD + | Self::FKP | Self::GBP + | Self::GEL | Self::GHS | Self::GIP | Self::GMD @@ -747,6 +804,7 @@ impl Currency { | Self::IDR | Self::ILS | Self::INR + | Self::IQD | Self::JMD | Self::JOD | Self::KES @@ -760,17 +818,20 @@ impl Currency { | Self::LKR | Self::LRD | Self::LSL + | Self::LYD | Self::MAD | Self::MDL | Self::MKD | Self::MMK | Self::MNT | Self::MOP + | Self::MRU | Self::MUR | Self::MVR | Self::MWK | Self::MXN | Self::MYR + | Self::MZN | Self::NAD | Self::NGN | Self::NIO @@ -778,6 +839,7 @@ impl Currency { | Self::NPR | Self::NZD | Self::OMR + | Self::PAB | Self::PEN | Self::PGK | Self::PHP @@ -785,42 +847,60 @@ impl Currency { | Self::PLN | Self::QAR | Self::RON + | Self::RSD | Self::RUB | Self::SAR + | Self::SBD | Self::SCR | Self::SEK | Self::SGD + | Self::SHP + | Self::SLE | Self::SLL | Self::SOS + | Self::SRD | Self::SSP + | Self::STN | Self::SVC | Self::SZL | Self::THB + | Self::TND + | Self::TOP | Self::TRY | Self::TTD | Self::TWD | Self::TZS + | Self::UAH | Self::USD | Self::UYU | Self::UZS + | Self::VES + | Self::WST + | Self::XCD | Self::YER - | Self::ZAR => false, + | Self::ZAR + | Self::ZMW => false, } } pub fn is_three_decimal_currency(self) -> bool { match self { - Self::BHD | Self::JOD | Self::KWD | Self::OMR => true, + Self::BHD | Self::IQD | Self::JOD | Self::KWD | Self::LYD | Self::OMR | Self::TND => { + true + } Self::AED | Self::ALL | Self::AMD + | Self::AOA | Self::ANG | Self::ARS | Self::AUD | Self::AWG | Self::AZN + | Self::BAM | Self::BBD | Self::BDT + | Self::BGN | Self::BIF | Self::BMD | Self::BND @@ -828,6 +908,7 @@ impl Currency { | Self::BRL | Self::BSD | Self::BWP + | Self::BYN | Self::BZD | Self::CAD | Self::CHF @@ -836,6 +917,7 @@ impl Currency { | Self::COP | Self::CRC | Self::CUP + | Self::CVE | Self::CZK | Self::DJF | Self::DKK @@ -845,7 +927,9 @@ impl Currency { | Self::ETB | Self::EUR | Self::FJD + | Self::FKP | Self::GBP + | Self::GEL | Self::GHS | Self::GIP | Self::GMD @@ -881,17 +965,20 @@ impl Currency { | Self::MMK | Self::MNT | Self::MOP + | Self::MRU | Self::MUR | Self::MVR | Self::MWK | Self::MXN | Self::MYR + | Self::MZN | Self::NAD | Self::NGN | Self::NIO | Self::NOK | Self::NPR | Self::NZD + | Self::PAB | Self::PEN | Self::PGK | Self::PHP @@ -900,33 +987,45 @@ impl Currency { | Self::PYG | Self::QAR | Self::RON + | Self::RSD | Self::RUB | Self::RWF | Self::SAR + | Self::SBD | Self::SCR | Self::SEK | Self::SGD + | Self::SHP + | Self::SLE | Self::SLL | Self::SOS + | Self::SRD | Self::SSP + | Self::STN | Self::SVC | Self::SZL | Self::THB + | Self::TOP | Self::TRY | Self::TTD | Self::TWD | Self::TZS + | Self::UAH | Self::UGX | Self::USD | Self::UYU | Self::UZS + | Self::VES | Self::VND | Self::VUV + | Self::WST | Self::XAF + | Self::XCD | Self::XPF | Self::XOF | Self::YER - | Self::ZAR => false, + | Self::ZAR + | Self::ZMW => false, } } } diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml index d84956fe2f76..d2c99080bf64 100644 --- a/crates/currency_conversion/Cargo.toml +++ b/crates/currency_conversion/Cargo.toml @@ -11,6 +11,6 @@ common_enums = { version = "0.1.0", path = "../common_enums", package = "common_ # Third party crates rust_decimal = "1.29" -rusty-money = { version = "0.4.0", features = ["iso", "crypto"] } +rusty-money = { git = "https://github.com/varunsrin/rusty_money", rev = "bbc0150742a0fff905225ff11ee09388e9babdcc", features = ["iso", "crypto"] } serde = { version = "1.0.193", features = ["derive"] } thiserror = "1.0.43" diff --git a/crates/currency_conversion/src/types.rs b/crates/currency_conversion/src/types.rs index fec25b9fc601..a84520dca0ad 100644 --- a/crates/currency_conversion/src/types.rs +++ b/crates/currency_conversion/src/types.rs @@ -81,12 +81,15 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::ALL => iso::ALL, Currency::AMD => iso::AMD, Currency::ANG => iso::ANG, + Currency::AOA => iso::AOA, Currency::ARS => iso::ARS, Currency::AUD => iso::AUD, Currency::AWG => iso::AWG, Currency::AZN => iso::AZN, + Currency::BAM => iso::BAM, Currency::BBD => iso::BBD, Currency::BDT => iso::BDT, + Currency::BGN => iso::BGN, Currency::BHD => iso::BHD, Currency::BIF => iso::BIF, Currency::BMD => iso::BMD, @@ -95,6 +98,7 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::BRL => iso::BRL, Currency::BSD => iso::BSD, Currency::BWP => iso::BWP, + Currency::BYN => iso::BYN, Currency::BZD => iso::BZD, Currency::CAD => iso::CAD, Currency::CHF => iso::CHF, @@ -103,6 +107,7 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::COP => iso::COP, Currency::CRC => iso::CRC, Currency::CUP => iso::CUP, + Currency::CVE => iso::CVE, Currency::CZK => iso::CZK, Currency::DJF => iso::DJF, Currency::DKK => iso::DKK, @@ -112,7 +117,9 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::ETB => iso::ETB, Currency::EUR => iso::EUR, Currency::FJD => iso::FJD, + Currency::FKP => iso::FKP, Currency::GBP => iso::GBP, + Currency::GEL => iso::GEL, Currency::GHS => iso::GHS, Currency::GIP => iso::GIP, Currency::GMD => iso::GMD, @@ -127,6 +134,7 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::IDR => iso::IDR, Currency::ILS => iso::ILS, Currency::INR => iso::INR, + Currency::IQD => iso::IQD, Currency::JMD => iso::JMD, Currency::JOD => iso::JOD, Currency::JPY => iso::JPY, @@ -143,6 +151,7 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::LKR => iso::LKR, Currency::LRD => iso::LRD, Currency::LSL => iso::LSL, + Currency::LYD => iso::LYD, Currency::MAD => iso::MAD, Currency::MDL => iso::MDL, Currency::MGA => iso::MGA, @@ -150,11 +159,13 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::MMK => iso::MMK, Currency::MNT => iso::MNT, Currency::MOP => iso::MOP, + Currency::MRU => iso::MRU, Currency::MUR => iso::MUR, Currency::MVR => iso::MVR, Currency::MWK => iso::MWK, Currency::MXN => iso::MXN, Currency::MYR => iso::MYR, + Currency::MZN => iso::MZN, Currency::NAD => iso::NAD, Currency::NGN => iso::NGN, Currency::NIO => iso::NIO, @@ -162,6 +173,7 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::NPR => iso::NPR, Currency::NZD => iso::NZD, Currency::OMR => iso::OMR, + Currency::PAB => iso::PAB, Currency::PEN => iso::PEN, Currency::PGK => iso::PGK, Currency::PHP => iso::PHP, @@ -170,32 +182,45 @@ pub fn currency_match(currency: Currency) -> &'static iso::Currency { Currency::PYG => iso::PYG, Currency::QAR => iso::QAR, Currency::RON => iso::RON, + Currency::RSD => iso::RSD, Currency::RUB => iso::RUB, Currency::RWF => iso::RWF, Currency::SAR => iso::SAR, + Currency::SBD => iso::SBD, Currency::SCR => iso::SCR, Currency::SEK => iso::SEK, Currency::SGD => iso::SGD, + Currency::SHP => iso::SHP, + Currency::SLE => iso::SLE, Currency::SLL => iso::SLL, Currency::SOS => iso::SOS, + Currency::SRD => iso::SRD, Currency::SSP => iso::SSP, + Currency::STN => iso::STN, Currency::SVC => iso::SVC, Currency::SZL => iso::SZL, Currency::THB => iso::THB, + Currency::TND => iso::TND, + Currency::TOP => iso::TOP, Currency::TTD => iso::TTD, Currency::TRY => iso::TRY, Currency::TWD => iso::TWD, Currency::TZS => iso::TZS, + Currency::UAH => iso::UAH, Currency::UGX => iso::UGX, Currency::USD => iso::USD, Currency::UYU => iso::UYU, Currency::UZS => iso::UZS, + Currency::VES => iso::VES, Currency::VND => iso::VND, Currency::VUV => iso::VUV, + Currency::WST => iso::WST, Currency::XAF => iso::XAF, + Currency::XCD => iso::XCD, Currency::XOF => iso::XOF, Currency::XPF => iso::XPF, Currency::YER => iso::YER, Currency::ZAR => iso::ZAR, + Currency::ZMW => iso::ZMW, } } diff --git a/crates/kgraph_utils/src/transformers.rs b/crates/kgraph_utils/src/transformers.rs index b1636418aa17..5bcb64fd8755 100644 --- a/crates/kgraph_utils/src/transformers.rs +++ b/crates/kgraph_utils/src/transformers.rs @@ -312,12 +312,15 @@ impl IntoDirValue for api_enums::Currency { Self::ALL => Ok(dirval!(PaymentCurrency = ALL)), Self::AMD => Ok(dirval!(PaymentCurrency = AMD)), Self::ANG => Ok(dirval!(PaymentCurrency = ANG)), + Self::AOA => Ok(dirval!(PaymentCurrency = AOA)), Self::ARS => Ok(dirval!(PaymentCurrency = ARS)), Self::AUD => Ok(dirval!(PaymentCurrency = AUD)), Self::AWG => Ok(dirval!(PaymentCurrency = AWG)), Self::AZN => Ok(dirval!(PaymentCurrency = AZN)), + Self::BAM => Ok(dirval!(PaymentCurrency = BAM)), Self::BBD => Ok(dirval!(PaymentCurrency = BBD)), Self::BDT => Ok(dirval!(PaymentCurrency = BDT)), + Self::BGN => Ok(dirval!(PaymentCurrency = BGN)), Self::BHD => Ok(dirval!(PaymentCurrency = BHD)), Self::BIF => Ok(dirval!(PaymentCurrency = BIF)), Self::BMD => Ok(dirval!(PaymentCurrency = BMD)), @@ -326,6 +329,7 @@ impl IntoDirValue for api_enums::Currency { Self::BRL => Ok(dirval!(PaymentCurrency = BRL)), Self::BSD => Ok(dirval!(PaymentCurrency = BSD)), Self::BWP => Ok(dirval!(PaymentCurrency = BWP)), + Self::BYN => Ok(dirval!(PaymentCurrency = BYN)), Self::BZD => Ok(dirval!(PaymentCurrency = BZD)), Self::CAD => Ok(dirval!(PaymentCurrency = CAD)), Self::CHF => Ok(dirval!(PaymentCurrency = CHF)), @@ -334,6 +338,7 @@ impl IntoDirValue for api_enums::Currency { Self::COP => Ok(dirval!(PaymentCurrency = COP)), Self::CRC => Ok(dirval!(PaymentCurrency = CRC)), Self::CUP => Ok(dirval!(PaymentCurrency = CUP)), + Self::CVE => Ok(dirval!(PaymentCurrency = CVE)), Self::CZK => Ok(dirval!(PaymentCurrency = CZK)), Self::DJF => Ok(dirval!(PaymentCurrency = DJF)), Self::DKK => Ok(dirval!(PaymentCurrency = DKK)), @@ -343,7 +348,9 @@ impl IntoDirValue for api_enums::Currency { Self::ETB => Ok(dirval!(PaymentCurrency = ETB)), Self::EUR => Ok(dirval!(PaymentCurrency = EUR)), Self::FJD => Ok(dirval!(PaymentCurrency = FJD)), + Self::FKP => Ok(dirval!(PaymentCurrency = FKP)), Self::GBP => Ok(dirval!(PaymentCurrency = GBP)), + Self::GEL => Ok(dirval!(PaymentCurrency = GEL)), Self::GHS => Ok(dirval!(PaymentCurrency = GHS)), Self::GIP => Ok(dirval!(PaymentCurrency = GIP)), Self::GMD => Ok(dirval!(PaymentCurrency = GMD)), @@ -358,6 +365,7 @@ impl IntoDirValue for api_enums::Currency { Self::IDR => Ok(dirval!(PaymentCurrency = IDR)), Self::ILS => Ok(dirval!(PaymentCurrency = ILS)), Self::INR => Ok(dirval!(PaymentCurrency = INR)), + Self::IQD => Ok(dirval!(PaymentCurrency = IQD)), Self::JMD => Ok(dirval!(PaymentCurrency = JMD)), Self::JOD => Ok(dirval!(PaymentCurrency = JOD)), Self::JPY => Ok(dirval!(PaymentCurrency = JPY)), @@ -374,6 +382,7 @@ impl IntoDirValue for api_enums::Currency { Self::LKR => Ok(dirval!(PaymentCurrency = LKR)), Self::LRD => Ok(dirval!(PaymentCurrency = LRD)), Self::LSL => Ok(dirval!(PaymentCurrency = LSL)), + Self::LYD => Ok(dirval!(PaymentCurrency = LYD)), Self::MAD => Ok(dirval!(PaymentCurrency = MAD)), Self::MDL => Ok(dirval!(PaymentCurrency = MDL)), Self::MGA => Ok(dirval!(PaymentCurrency = MGA)), @@ -381,11 +390,13 @@ impl IntoDirValue for api_enums::Currency { Self::MMK => Ok(dirval!(PaymentCurrency = MMK)), Self::MNT => Ok(dirval!(PaymentCurrency = MNT)), Self::MOP => Ok(dirval!(PaymentCurrency = MOP)), + Self::MRU => Ok(dirval!(PaymentCurrency = MRU)), Self::MUR => Ok(dirval!(PaymentCurrency = MUR)), Self::MVR => Ok(dirval!(PaymentCurrency = MVR)), Self::MWK => Ok(dirval!(PaymentCurrency = MWK)), Self::MXN => Ok(dirval!(PaymentCurrency = MXN)), Self::MYR => Ok(dirval!(PaymentCurrency = MYR)), + Self::MZN => Ok(dirval!(PaymentCurrency = MZN)), Self::NAD => Ok(dirval!(PaymentCurrency = NAD)), Self::NGN => Ok(dirval!(PaymentCurrency = NGN)), Self::NIO => Ok(dirval!(PaymentCurrency = NIO)), @@ -393,6 +404,7 @@ impl IntoDirValue for api_enums::Currency { Self::NPR => Ok(dirval!(PaymentCurrency = NPR)), Self::NZD => Ok(dirval!(PaymentCurrency = NZD)), Self::OMR => Ok(dirval!(PaymentCurrency = OMR)), + Self::PAB => Ok(dirval!(PaymentCurrency = PAB)), Self::PEN => Ok(dirval!(PaymentCurrency = PEN)), Self::PGK => Ok(dirval!(PaymentCurrency = PGK)), Self::PHP => Ok(dirval!(PaymentCurrency = PHP)), @@ -401,33 +413,46 @@ impl IntoDirValue for api_enums::Currency { Self::PYG => Ok(dirval!(PaymentCurrency = PYG)), Self::QAR => Ok(dirval!(PaymentCurrency = QAR)), Self::RON => Ok(dirval!(PaymentCurrency = RON)), + Self::RSD => Ok(dirval!(PaymentCurrency = RSD)), Self::RUB => Ok(dirval!(PaymentCurrency = RUB)), Self::RWF => Ok(dirval!(PaymentCurrency = RWF)), Self::SAR => Ok(dirval!(PaymentCurrency = SAR)), + Self::SBD => Ok(dirval!(PaymentCurrency = SBD)), Self::SCR => Ok(dirval!(PaymentCurrency = SCR)), Self::SEK => Ok(dirval!(PaymentCurrency = SEK)), Self::SGD => Ok(dirval!(PaymentCurrency = SGD)), + Self::SHP => Ok(dirval!(PaymentCurrency = SHP)), + Self::SLE => Ok(dirval!(PaymentCurrency = SLE)), Self::SLL => Ok(dirval!(PaymentCurrency = SLL)), Self::SOS => Ok(dirval!(PaymentCurrency = SOS)), + Self::SRD => Ok(dirval!(PaymentCurrency = SRD)), Self::SSP => Ok(dirval!(PaymentCurrency = SSP)), + Self::STN => Ok(dirval!(PaymentCurrency = STN)), Self::SVC => Ok(dirval!(PaymentCurrency = SVC)), Self::SZL => Ok(dirval!(PaymentCurrency = SZL)), Self::THB => Ok(dirval!(PaymentCurrency = THB)), + Self::TND => Ok(dirval!(PaymentCurrency = TND)), + Self::TOP => Ok(dirval!(PaymentCurrency = TOP)), Self::TRY => Ok(dirval!(PaymentCurrency = TRY)), Self::TTD => Ok(dirval!(PaymentCurrency = TTD)), Self::TWD => Ok(dirval!(PaymentCurrency = TWD)), Self::TZS => Ok(dirval!(PaymentCurrency = TZS)), + Self::UAH => Ok(dirval!(PaymentCurrency = UAH)), Self::UGX => Ok(dirval!(PaymentCurrency = UGX)), Self::USD => Ok(dirval!(PaymentCurrency = USD)), Self::UYU => Ok(dirval!(PaymentCurrency = UYU)), Self::UZS => Ok(dirval!(PaymentCurrency = UZS)), + Self::VES => Ok(dirval!(PaymentCurrency = VES)), Self::VND => Ok(dirval!(PaymentCurrency = VND)), Self::VUV => Ok(dirval!(PaymentCurrency = VUV)), + Self::WST => Ok(dirval!(PaymentCurrency = WST)), Self::XAF => Ok(dirval!(PaymentCurrency = XAF)), + Self::XCD => Ok(dirval!(PaymentCurrency = XCD)), Self::XOF => Ok(dirval!(PaymentCurrency = XOF)), Self::XPF => Ok(dirval!(PaymentCurrency = XPF)), Self::YER => Ok(dirval!(PaymentCurrency = YER)), Self::ZAR => Ok(dirval!(PaymentCurrency = ZAR)), + Self::ZMW => Ok(dirval!(PaymentCurrency = ZMW)), } } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 146a1ace28e6..dd6eaa104cb3 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -1,7 +1,6 @@ use std::{ collections::{HashMap, HashSet}, path::PathBuf, - str::FromStr, }; #[cfg(feature = "olap")] @@ -20,7 +19,7 @@ use redis_interface::RedisSettings; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use rust_decimal::Decimal; use scheduler::SchedulerSettings; -use serde::{de::Error, Deserialize, Deserializer}; +use serde::Deserialize; use storage_impl::config::QueueStrategy; #[cfg(feature = "olap")] @@ -191,7 +190,7 @@ pub struct ApplepayMerchantConfigs { #[derive(Debug, Deserialize, Clone, Default)] pub struct MultipleApiVersionSupportedConnectors { - #[serde(deserialize_with = "connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub supported_connectors: HashSet, } @@ -205,42 +204,13 @@ pub struct TempLockerEnableConfig(pub HashMap, #[cfg(feature = "payouts")] - #[serde(deserialize_with = "payout_connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub payout_connector_list: HashSet, } -fn connector_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - Ok(value - .trim() - .split(',') - .flat_map(api_models::enums::Connector::from_str) - .collect()) -} - -#[cfg(feature = "payouts")] -fn payout_connector_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - Ok(value - .trim() - .split(',') - .flat_map(api_models::enums::PayoutConnectors::from_str) - .collect()) -} - #[cfg(feature = "dummy_connector")] #[derive(Debug, Deserialize, Clone, Default)] pub struct DummyConnector { @@ -281,13 +251,13 @@ pub struct SupportedPaymentMethodTypesForMandate( #[derive(Debug, Deserialize, Clone)] pub struct SupportedConnectorsForMandate { - #[serde(deserialize_with = "connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub connector_list: HashSet, } #[derive(Debug, Deserialize, Clone, Default)] pub struct PaymentMethodTokenFilter { - #[serde(deserialize_with = "pm_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub payment_method: HashSet, pub payment_method_type: Option, pub long_lived_token: bool, @@ -304,7 +274,7 @@ pub enum ApplePayPreDecryptFlow { #[derive(Debug, Deserialize, Clone, Default)] pub struct TempLockerEnablePaymentMethodFilter { - #[serde(deserialize_with = "pm_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub payment_method: HashSet, } @@ -316,44 +286,14 @@ pub struct TempLockerEnablePaymentMethodFilter { rename_all = "snake_case" )] pub enum PaymentMethodTypeTokenFilter { - #[serde(deserialize_with = "pm_type_deser")] + #[serde(deserialize_with = "deserialize_hashset")] EnableOnly(HashSet), - #[serde(deserialize_with = "pm_type_deser")] + #[serde(deserialize_with = "deserialize_hashset")] DisableOnly(HashSet), #[default] AllAccepted, } -fn pm_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - value - .trim() - .split(',') - .map(diesel_models::enums::PaymentMethod::from_str) - .collect::>() - .map_err(D::Error::custom) -} - -fn pm_type_deser<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - value - .trim() - .split(',') - .map(diesel_models::enums::PaymentMethodType::from_str) - .collect::>() - .map_err(D::Error::custom) -} - #[derive(Debug, Deserialize, Clone, Default)] pub struct BankRedirectConfig( pub HashMap, @@ -363,7 +303,7 @@ pub struct ConnectorBankNames(pub HashMap); #[derive(Debug, Deserialize, Clone)] pub struct BanksVector { - #[serde(deserialize_with = "bank_vec_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub banks: HashSet, } @@ -385,9 +325,9 @@ pub enum PaymentMethodFilterKey { #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct CurrencyCountryFlowFilter { - #[serde(deserialize_with = "currency_set_deser")] + #[serde(deserialize_with = "deserialize_optional_hashset")] pub currency: Option>, - #[serde(deserialize_with = "string_set_deser")] + #[serde(deserialize_with = "deserialize_optional_hashset")] pub country: Option>, pub not_available_flows: Option, } @@ -416,58 +356,6 @@ pub struct RequiredFieldFinal { pub common: HashMap, } -fn string_set_deser<'a, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'a>, -{ - let value = >::deserialize(deserializer)?; - Ok(value.and_then(|inner| { - let list = inner - .trim() - .split(',') - .flat_map(api_models::enums::CountryAlpha2::from_str) - .collect::>(); - match list.len() { - 0 => None, - _ => Some(list), - } - })) -} - -fn currency_set_deser<'a, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'a>, -{ - let value = >::deserialize(deserializer)?; - Ok(value.and_then(|inner| { - let list = inner - .trim() - .split(',') - .flat_map(api_models::enums::Currency::from_str) - .collect::>(); - match list.len() { - 0 => None, - _ => Some(list), - } - })) -} - -fn bank_vec_deser<'a, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - Ok(value - .trim() - .split(',') - .flat_map(api_models::enums::BankNames::from_str) - .collect()) -} - #[derive(Debug, Default, Deserialize, Clone)] #[serde(default)] pub struct Secrets { @@ -723,13 +611,13 @@ pub struct ApiKeys { #[derive(Debug, Deserialize, Clone, Default)] pub struct DelayedSessionConfig { - #[serde(deserialize_with = "deser_to_get_connectors")] + #[serde(deserialize_with = "deserialize_hashset")] pub connectors_with_delayed_session_response: HashSet, } #[derive(Debug, Deserialize, Clone, Default)] pub struct WebhookSourceVerificationCall { - #[serde(deserialize_with = "connector_deser")] + #[serde(deserialize_with = "deserialize_hashset")] pub connectors_with_webhook_source_verification_call: HashSet, } @@ -746,21 +634,6 @@ pub struct ConnectorRequestReferenceIdConfig { pub merchant_ids_send_payment_id_as_connector_request_id: HashSet, } -fn deser_to_get_connectors<'a, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'a>, -{ - let value = ::deserialize(deserializer)?; - value - .trim() - .split(',') - .map(api_models::enums::Connector::from_str) - .collect::>() - .map_err(D::Error::custom) -} - impl Settings { pub fn new() -> ApplicationResult { Self::with_config_path(None) @@ -854,24 +727,6 @@ impl Settings { } } -#[cfg(test)] -mod payment_method_deserialization_test { - #![allow(clippy::unwrap_used)] - use serde::de::{ - value::{Error as ValueError, StrDeserializer}, - IntoDeserializer, - }; - - use super::*; - - #[test] - fn test_pm_deserializer() { - let deserializer: StrDeserializer<'_, ValueError> = "wallet,card".into_deserializer(); - let test_pm = pm_deser(deserializer); - assert!(test_pm.is_ok()) - } -} - #[cfg(feature = "payouts")] #[derive(Debug, Deserialize, Clone, Default)] pub struct Payouts { @@ -886,7 +741,7 @@ pub struct LockSettings { } impl<'de> Deserialize<'de> for LockSettings { - fn deserialize>(deserializer: D) -> Result { + fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct Inner { @@ -921,3 +776,124 @@ pub struct PayPalOnboarding { pub partner_id: masking::Secret, pub enabled: bool, } + +fn deserialize_hashset_inner(value: impl AsRef) -> Result, String> +where + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + let (values, errors) = value + .as_ref() + .trim() + .split(',') + .map(|s| { + T::from_str(s.trim()).map_err(|error| { + format!( + "Unable to deserialize `{}` as `{}`: {error}", + s.trim(), + std::any::type_name::() + ) + }) + }) + .fold( + (HashSet::new(), Vec::new()), + |(mut values, mut errors), result| match result { + Ok(t) => { + values.insert(t); + (values, errors) + } + Err(error) => { + errors.push(error); + (values, errors) + } + }, + ); + if !errors.is_empty() { + Err(format!("Some errors occurred:\n{}", errors.join("\n"))) + } else { + Ok(values) + } +} + +fn deserialize_hashset<'a, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'a>, + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + use serde::de::Error; + + deserialize_hashset_inner(::deserialize(deserializer)?).map_err(D::Error::custom) +} + +fn deserialize_optional_hashset<'a, D, T>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'a>, + T: Eq + std::str::FromStr + std::hash::Hash, + ::Err: std::fmt::Display, +{ + use serde::de::Error; + + >::deserialize(deserializer).map(|value| { + value.map_or(Ok(None), |inner: String| { + let list = deserialize_hashset_inner(inner).map_err(D::Error::custom)?; + match list.len() { + 0 => Ok(None), + _ => Ok(Some(list)), + } + }) + })? +} + +#[cfg(test)] +mod hashset_deserialization_test { + #![allow(clippy::unwrap_used)] + use std::collections::HashSet; + + use serde::de::{ + value::{Error as ValueError, StrDeserializer}, + IntoDeserializer, + }; + + use super::deserialize_hashset; + + #[test] + fn test_payment_method_hashset_deserializer() { + use diesel_models::enums::PaymentMethod; + + let deserializer: StrDeserializer<'_, ValueError> = "wallet,card".into_deserializer(); + let payment_methods = deserialize_hashset::<'_, _, PaymentMethod>(deserializer); + let expected_payment_methods = HashSet::from([PaymentMethod::Wallet, PaymentMethod::Card]); + + assert!(payment_methods.is_ok()); + assert_eq!(payment_methods.unwrap(), expected_payment_methods); + } + + #[test] + fn test_payment_method_hashset_deserializer_with_spaces() { + use diesel_models::enums::PaymentMethod; + + let deserializer: StrDeserializer<'_, ValueError> = + "wallet, card, bank_debit".into_deserializer(); + let payment_methods = deserialize_hashset::<'_, _, PaymentMethod>(deserializer); + let expected_payment_methods = HashSet::from([ + PaymentMethod::Wallet, + PaymentMethod::Card, + PaymentMethod::BankDebit, + ]); + + assert!(payment_methods.is_ok()); + assert_eq!(payment_methods.unwrap(), expected_payment_methods); + } + + #[test] + fn test_payment_method_hashset_deserializer_error() { + use diesel_models::enums::PaymentMethod; + + let deserializer: StrDeserializer<'_, ValueError> = + "wallet, card, unknown".into_deserializer(); + let payment_methods = deserialize_hashset::<'_, _, PaymentMethod>(deserializer); + + assert!(payment_methods.is_err()); + } +} diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 09cb5fe14040..5696b6fd692b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7048,12 +7048,15 @@ "ALL", "AMD", "ANG", + "AOA", "ARS", "AUD", "AWG", "AZN", + "BAM", "BBD", "BDT", + "BGN", "BHD", "BIF", "BMD", @@ -7062,6 +7065,7 @@ "BRL", "BSD", "BWP", + "BYN", "BZD", "CAD", "CHF", @@ -7070,6 +7074,7 @@ "COP", "CRC", "CUP", + "CVE", "CZK", "DJF", "DKK", @@ -7079,7 +7084,9 @@ "ETB", "EUR", "FJD", + "FKP", "GBP", + "GEL", "GHS", "GIP", "GMD", @@ -7094,6 +7101,7 @@ "IDR", "ILS", "INR", + "IQD", "JMD", "JOD", "JPY", @@ -7110,6 +7118,7 @@ "LKR", "LRD", "LSL", + "LYD", "MAD", "MDL", "MGA", @@ -7117,11 +7126,13 @@ "MMK", "MNT", "MOP", + "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", + "MZN", "NAD", "NGN", "NIO", @@ -7129,6 +7140,7 @@ "NPR", "NZD", "OMR", + "PAB", "PEN", "PGK", "PHP", @@ -7137,33 +7149,46 @@ "PYG", "QAR", "RON", + "RSD", "RUB", "RWF", "SAR", + "SBD", "SCR", "SEK", "SGD", + "SHP", + "SLE", "SLL", "SOS", + "SRD", "SSP", + "STN", "SVC", "SZL", "THB", + "TND", + "TOP", "TRY", "TTD", "TWD", "TZS", + "UAH", "UGX", "USD", "UYU", "UZS", + "VES", "VND", "VUV", + "WST", "XAF", + "XCD", "XOF", "XPF", "YER", - "ZAR" + "ZAR", + "ZMW" ] }, "CustomerAcceptance": { From 3d55e3ba45619978e8ca9e5012c156dc017d2879 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:26:55 +0530 Subject: [PATCH 408/443] feat(pm_list): add required fields for sofort (#3192) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- config/config.example.toml | 15 +- config/development.toml | 3 +- config/docker_compose.toml | 3 +- crates/api_models/src/payments.rs | 6 +- crates/router/src/configs/defaults.rs | 293 +++++++++++++++++- .../router/src/connector/aci/transformers.rs | 6 +- .../src/connector/adyen/transformers.rs | 5 +- .../src/connector/paypal/transformers.rs | 11 +- .../src/connector/stripe/transformers.rs | 87 +++--- loadtest/config/development.toml | 3 +- openapi/openapi_spec.json | 14 +- 11 files changed, 381 insertions(+), 65 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index d53a6e28ef6f..00c325f058e6 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -374,13 +374,14 @@ slack_invite_url = "https://www.example.com/" # Slack invite url for hyperswit discord_invite_url = "https://www.example.com/" # Discord invite url for hyperswitch [mandates.supported_payment_methods] -card.credit = { connector_list = "stripe,adyen,cybersource" } # Mandate supported payment method type and connector for card -wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets -pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later -bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit -bank_redirect.ideal = { connector_list = "stripe,adyen" } # Mandate supported payment method type and connector for bank_redirect +card.credit = { connector_list = "stripe,adyen,cybersource" } # Mandate supported payment method type and connector for card +wallet.paypal = { connector_list = "adyen" } # Mandate supported payment method type and connector for wallets +pay_later.klarna = { connector_list = "adyen" } # Mandate supported payment method type and connector for pay_later +bank_debit.ach = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} # Mandate supported payment method type and connector for bank_redirect +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} # Required fields info used while listing the payment_method_data diff --git a/config/development.toml b/config/development.toml index 7c494ea28521..d447930b5902 100644 --- a/config/development.toml +++ b/config/development.toml @@ -474,7 +474,8 @@ card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,global bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} -bank_redirect.ideal = {connector_list = "stripe,adyen"} +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [connector_request_reference_id_config] merchant_ids_send_payment_id_as_connector_request_id = [] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 79039e136792..d468f1dd412f 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -340,7 +340,8 @@ card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalp bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} -bank_redirect.ideal = {connector_list = "stripe,adyen"} +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [connector_customer] connector_list = "gocardless,stax,stripe" diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 8c27f498d7ad..f8d31c6e414e 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1336,15 +1336,15 @@ pub enum BankRedirectData { }, Sofort { /// The billing details for bank redirection - billing_details: BankRedirectBilling, + billing_details: Option, /// The country for bank payment #[schema(value_type = CountryAlpha2, example = "US")] - country: api_enums::CountryAlpha2, + country: Option, /// The preferred language #[schema(example = "en")] - preferred_language: String, + preferred_language: Option, }, Trustly { /// The country for bank payment diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index e4a470d0da35..b6d8fa114b9a 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4750,14 +4750,303 @@ impl Default for super::settings::RequiredFields { ConnectorFields { fields: HashMap::from([ ( - enums::Connector::Stripe, + enums::Connector::Aci, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ("payment_method_data.bank_redirect.sofort.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "ES".to_string(), + "GB".to_string(), + "SE".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "CH".to_string(), + "BE".to_string(), + "FR".to_string(), + "FI".to_string(), + "IT".to_string(), + "PL".to_string(), + ] + }, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Adyen, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), common: HashMap::new(), } ), - ]), + ( + enums::Connector::Globalpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::from([ + ("billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry { + options: vec![ + "AT".to_string(), + "BE".to_string(), + "DE".to_string(), + "ES".to_string(), + "IT".to_string(), + "NL".to_string(), + ] + }, + value: None, + } + ) + ]), + } + ), + ( + enums::Connector::Mollie, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nexinets, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate:HashMap::from([ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ES".to_string(), + "GB".to_string(), + "IT".to_string(), + "DE".to_string(), + "FR".to_string(), + "AT".to_string(), + "BE".to_string(), + "NL".to_string(), + "BE".to_string(), + "SK".to_string(), + ] + }, + value: None, + } + )] + ), + common: HashMap::new(), + } + ), + ( + enums::Connector::Paypal, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ("payment_method_data.bank_redirect.sofort.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "ES".to_string(), + "GB".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "BE".to_string(), + ] + }, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Shift4, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::from([ + ( + "payment_method_data.bank_redirect.sofort.billing_details.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.billing_details.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.billing_details.billing_name".to_string(), + display_name: "billing_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ) + ]), + non_mandate : HashMap::from([ + ("payment_method_data.bank_redirect.sofort.country".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.sofort.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserCountry { + options: vec![ + "ES".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "BE".to_string(), + ] + }, + value: None, + } + )]), + common: HashMap::new( + + ), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry { + options: vec![ + "ES".to_string(), + "GB".to_string(), + "SE".to_string(), + "AT".to_string(), + "NL".to_string(), + "DE".to_string(), + "CH".to_string(), + "BE".to_string(), + "FR".to_string(), + "FI".to_string(), + "IT".to_string(), + "PL".to_string(), + ] + }, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ]), }, ), ( diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index f34909f5489f..97cef72b02a7 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -202,7 +202,11 @@ impl api_models::payments::BankRedirectData::Sofort { country, .. } => { Self::BankRedirect(Box::new(BankRedirectionPMData { payment_brand: PaymentBrand::Sofortueberweisung, - bank_account_country: Some(country.to_owned()), + bank_account_country: Some(country.to_owned().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "sofort.country", + }, + )?), bank_account_bank_name: None, bank_account_bic: None, bank_account_iban: None, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 76678a1b33b0..8da5d15c4446 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2715,10 +2715,7 @@ fn get_redirect_extra_details( country, preferred_language, .. - } => Ok(( - Some(preferred_language.to_string()), - Some(country.to_owned()), - )), + } => Ok((preferred_language.clone(), *country)), api_models::payments::BankRedirectData::OpenBankingUk { country, .. } => { let country = country.ok_or(errors::ConnectorError::MissingRequiredField { field_name: "country", diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index d384bc4cfd53..baf8f48279d9 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -360,8 +360,15 @@ fn get_payment_source( preferred_language: _, billing_details, } => Ok(PaymentSourceItem::Sofort(RedirectRequest { - name: billing_details.get_billing_name()?, - country_code: *country, + name: billing_details + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "sofort.billing_details", + })? + .get_billing_name()?, + country_code: country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "sofort.country", + })?, experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 1dbb310868a6..70582c41aa4d 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -290,7 +290,7 @@ pub struct StripeSofort { #[serde(rename = "payment_method_data[type]")] pub payment_method_data_type: StripePaymentMethodType, #[serde(rename = "payment_method_options[sofort][preferred_language]")] - preferred_language: String, + preferred_language: Option, #[serde(rename = "payment_method_data[sofort][country]")] country: api_enums::CountryAlpha2, } @@ -1115,36 +1115,10 @@ impl TryFrom<(&payments::BankRedirectData, Option)> for StripeBillingAddre }), payments::BankRedirectData::Ideal { billing_details, .. - } => { - let billing_name = billing_details - .clone() - .and_then(|billing_data| billing_data.billing_name.clone()); - - let billing_email = billing_details - .clone() - .and_then(|billing_data| billing_data.email.clone()); - match is_customer_initiated_mandate_payment { - Some(true) => Ok(Self { - name: Some(billing_name.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "billing_name", - }, - )?), - - email: Some(billing_email.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "billing_email", - }, - )?), - ..Self::default() - }), - Some(false) | None => Ok(Self { - name: billing_name, - email: billing_email, - ..Self::default() - }), - } - } + } => Ok(get_stripe_sepa_dd_mandate_billing_details( + billing_details, + is_customer_initiated_mandate_payment, + )?), payments::BankRedirectData::Przelewy24 { billing_details, .. } => Ok(Self { @@ -1183,11 +1157,11 @@ impl TryFrom<(&payments::BankRedirectData, Option)> for StripeBillingAddre } payments::BankRedirectData::Sofort { billing_details, .. - } => Ok(Self { - name: billing_details.billing_name.clone(), - email: billing_details.email.clone(), - ..Self::default() - }), + } => Ok(get_stripe_sepa_dd_mandate_billing_details( + billing_details, + is_customer_initiated_mandate_payment, + )?), + payments::BankRedirectData::Bizum {} | payments::BankRedirectData::Blik { .. } | payments::BankRedirectData::Interac { .. } @@ -1674,8 +1648,10 @@ impl TryFrom<&payments::BankRedirectData> for StripePaymentMethodData { } => Ok(Self::BankRedirect(StripeBankRedirectData::StripeSofort( Box::new(StripeSofort { payment_method_data_type, - country: country.to_owned(), - preferred_language: preferred_language.to_owned(), + country: country.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "sofort.country", + })?, + preferred_language: preferred_language.clone(), }), ))), payments::BankRedirectData::OnlineBankingFpx { .. } => { @@ -3593,6 +3569,41 @@ pub struct Evidence { pub submit: bool, } +// Mandates for bank redirects - ideal and sofort happens through sepa direct debit in stripe +fn get_stripe_sepa_dd_mandate_billing_details( + billing_details: &Option, + is_customer_initiated_mandate_payment: Option, +) -> Result { + let billing_name = billing_details + .clone() + .and_then(|billing_data| billing_data.billing_name.clone()); + + let billing_email = billing_details + .clone() + .and_then(|billing_data| billing_data.email.clone()); + match is_customer_initiated_mandate_payment { + Some(true) => Ok(StripeBillingAddress { + name: Some( + billing_name.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "billing_name", + })?, + ), + + email: Some( + billing_email.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "billing_email", + })?, + ), + ..StripeBillingAddress::default() + }), + Some(false) | None => Ok(StripeBillingAddress { + name: billing_name, + email: billing_email, + ..StripeBillingAddress::default() + }), + } +} + impl TryFrom<&types::SubmitEvidenceRouterData> for Evidence { type Error = error_stack::Report; fn try_from(item: &types::SubmitEvidenceRouterData) -> Result { diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 268ebd1d3ac9..954336070d58 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -246,7 +246,8 @@ card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalp bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} -bank_redirect.ideal = {connector_list = "stripe,adyen"} +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [analytics] source = "sqlx" diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 5696b6fd692b..d516eeed4cc7 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5367,13 +5367,16 @@ "sofort": { "type": "object", "required": [ - "billing_details", - "country", - "preferred_language" + "country" ], "properties": { "billing_details": { - "$ref": "#/components/schemas/BankRedirectBilling" + "allOf": [ + { + "$ref": "#/components/schemas/BankRedirectBilling" + } + ], + "nullable": true }, "country": { "$ref": "#/components/schemas/CountryAlpha2" @@ -5381,7 +5384,8 @@ "preferred_language": { "type": "string", "description": "The preferred language", - "example": "en" + "example": "en", + "nullable": true } } } From b2afdc35465426bd11428d8d4ac743617a443128 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:28:29 +0530 Subject: [PATCH 409/443] feat(connector): [Bluesnap] Metadata to connector metadata mapping (#3331) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connector/bluesnap/transformers.rs | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index 17cdf3b519bb..e98b98e874aa 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use api_models::{enums as api_enums, payments}; use base64::Engine; use common_utils::{ @@ -8,6 +10,7 @@ use common_utils::{ use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ connector::utils::{ @@ -17,10 +20,16 @@ use crate::{ consts, core::errors, pii::Secret, - types::{self, api, storage::enums, transformers::ForeignTryFrom}, + types::{ + self, api, + storage::enums, + transformers::{ForeignFrom, ForeignTryFrom}, + }, utils::{Encode, OptionExt}, }; +const DISPLAY_METADATA: &str = "Y"; + #[derive(Debug, Serialize)] pub struct BluesnapRouterData { pub amount: String, @@ -63,6 +72,21 @@ pub struct BluesnapPaymentsRequest { transaction_fraud_info: Option, card_holder_info: Option, merchant_transaction_id: Option, + transaction_meta_data: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BluesnapMetadata { + meta_data: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestMetadata { + meta_key: Option, + meta_value: Option, + is_visible: Option, } #[derive(Debug, Serialize)] @@ -241,6 +265,15 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, _ => BluesnapTxnType::AuthCapture, }; + let transaction_meta_data = + item.router_data + .request + .metadata + .as_ref() + .map(|metadata| BluesnapMetadata { + meta_data: Vec::::foreign_from(metadata.peek().to_owned()), + }); + let (payment_method, card_holder_info) = match item .router_data .request @@ -405,6 +438,7 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues }), card_holder_info, merchant_transaction_id: Some(item.router_data.connector_request_reference_id.clone()), + transaction_meta_data, }) } } @@ -569,6 +603,7 @@ pub struct BluesnapCompletePaymentsRequest { transaction_fraud_info: Option, card_holder_info: Option, merchant_transaction_id: Option, + transaction_meta_data: Option, } impl TryFrom<&BluesnapRouterData<&types::PaymentsCompleteAuthorizeRouterData>> @@ -590,6 +625,15 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsCompleteAuthorizeRouterData>> .parse_value("BluesnapRedirectionResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let transaction_meta_data = + item.router_data + .request + .metadata + .as_ref() + .map(|metadata| BluesnapMetadata { + meta_data: Vec::::foreign_from(metadata.peek().to_owned()), + }); + let pf_token = item .router_data .request @@ -637,6 +681,7 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsCompleteAuthorizeRouterData>> )?, merchant_transaction_id: Some(item.router_data.connector_request_reference_id.clone()), pf_token, + transaction_meta_data, }) } } @@ -1021,7 +1066,7 @@ pub struct BluesnapWebhookObjectResource { reversal_ref_num: Option, } -impl TryFrom for serde_json::Value { +impl TryFrom for Value { type Error = error_stack::Report; fn try_from(details: BluesnapWebhookObjectResource) -> Result { let (card_transaction_type, processing_status, transaction_id) = match details @@ -1118,3 +1163,19 @@ impl From for utils::ErrorCodeAndMessage { } } } + +impl ForeignFrom for Vec { + fn foreign_from(metadata: Value) -> Self { + let hashmap: HashMap, Option> = + serde_json::from_str(&metadata.to_string()).unwrap_or(HashMap::new()); + let mut vector: Self = Self::new(); + for (key, value) in hashmap { + vector.push(RequestMetadata { + meta_key: key, + meta_value: value.map(|field_value| field_value.to_string()), + is_visible: Some(DISPLAY_METADATA.to_string()), + }); + } + vector + } +} From 46c1822d0e367e59420c9d087428bc3b12794445 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:05:57 +0530 Subject: [PATCH 410/443] feat(config): Add iDEAL and Sofort Env Configs (#3492) --- config/deployments/integration_test.toml | 2 ++ config/deployments/production.toml | 2 ++ config/deployments/sandbox.toml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index d377b3359c94..b84546eff34f 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -117,6 +117,8 @@ pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource" wallet.paypal.connector_list = "adyen" +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [multiple_api_version_supported_connectors] supported_connectors = "braintree" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index d4671d3a99d2..d5479b4f02c1 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -117,6 +117,8 @@ pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource" wallet.paypal.connector_list = "adyen" +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [multiple_api_version_supported_connectors] supported_connectors = "braintree" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index abacd3ba5a16..c58eff29edb6 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -117,6 +117,8 @@ pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource" wallet.paypal.connector_list = "adyen" +bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} +bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} [multiple_api_version_supported_connectors] supported_connectors = "braintree" From a7bc8c655f5b745dccd4d818ac3ceb08c3b80c0e Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Tue, 30 Jan 2024 16:12:01 +0530 Subject: [PATCH 411/443] refactor(payment_link): segregated payment link in html css js files, sdk over flow issue, surcharge bug, block SPM customer call for payment link (#3410) Co-authored-by: Kashif Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> --- crates/api_models/src/payments.rs | 14 +- crates/common_utils/src/consts.rs | 6 +- crates/router/src/core/payment_link.rs | 173 +- .../src/core/payment_link/payment_link.html | 2130 ----------------- .../payment_link_initiate/payment_link.css | 785 ++++++ .../payment_link_initiate/payment_link.html | 197 ++ .../payment_link_initiate/payment_link.js | 906 +++++++ .../payment_link_status/status.css | 161 ++ .../payment_link_status/status.html | 26 + .../payment_link_status/status.js | 379 +++ .../router/src/core/payment_link/status.html | 355 --- crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 7 +- crates/router/src/routes/payment_link.rs | 30 + crates/router/src/services/api.rs | 77 +- crates/router_env/src/logger/types.rs | 2 + 16 files changed, 2743 insertions(+), 2509 deletions(-) delete mode 100644 crates/router/src/core/payment_link/payment_link.html create mode 100644 crates/router/src/core/payment_link/payment_link_initiate/payment_link.css create mode 100644 crates/router/src/core/payment_link/payment_link_initiate/payment_link.html create mode 100644 crates/router/src/core/payment_link/payment_link_initiate/payment_link.js create mode 100644 crates/router/src/core/payment_link/payment_link_status/status.css create mode 100644 crates/router/src/core/payment_link/payment_link_status/status.html create mode 100644 crates/router/src/core/payment_link/payment_link_status/status.js delete mode 100644 crates/router/src/core/payment_link/status.html diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f8d31c6e414e..c856ae327955 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3496,10 +3496,12 @@ pub struct PaymentLinkStatusDetails { pub merchant_name: String, #[serde(with = "common_utils::custom_serde::iso8601")] pub created: PrimitiveDateTime, - pub intent_status: api_enums::IntentStatus, - pub payment_link_status: PaymentLinkStatus, + pub status: PaymentLinkStatusWrap, pub error_code: Option, pub error_message: Option, + pub redirect: bool, + pub theme: String, + pub return_url: String, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -3583,3 +3585,11 @@ pub enum PaymentLinkStatus { Active, Expired, } + +#[derive(PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum PaymentLinkStatusWrap { + PaymentLinkStatus(PaymentLinkStatus), + IntentStatus(api_enums::IntentStatus), +} diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index cd24e430b76d..1e28d2c47f3c 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -37,10 +37,12 @@ pub const X_HS_LATENCY: &str = "x-hs-latency"; pub const DEFAULT_BACKGROUND_COLOR: &str = "#212E46"; /// Default product Img Link -pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; +pub const DEFAULT_PRODUCT_IMG: &str = + "https://live.hyperswitch.io/payment-link-assets/cart_placeholder.png"; /// Default Merchant Logo Link -pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; +pub const DEFAULT_MERCHANT_LOGO: &str = + "https://live.hyperswitch.io/payment-link-assets/Merchant_placeholder.png"; /// Redirect url for Prophetpay pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/tokenize/"; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 84cd726a7e49..5af948bcf3e0 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,4 +1,4 @@ -use api_models::admin as admin_types; +use api_models::{admin as admin_types, payments::PaymentLinkStatusWrap}; use common_utils::{ consts::{ DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_LAYOUT, @@ -89,10 +89,24 @@ pub async fn intiate_payment_link_flow( } }; + let profile_id = payment_link + .profile_id + .or(payment_intent.profile_id) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Profile id missing in payment link and payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(&profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() { payment_create_return_url } else { - merchant_account + business_profile .return_url .ok_or(errors::ApiErrorResponse::MissingRequiredField { field_name: "return_url", @@ -121,7 +135,7 @@ pub async fn intiate_payment_link_flow( let css_script = get_color_scheme_css(payment_link_config.clone()); let payment_link_status = check_payment_link_status(session_expiry); - if check_payment_link_invalid_conditions( + let is_terminal_state = check_payment_link_invalid_conditions( &payment_intent.status, &[ storage_enums::IntentStatus::Cancelled, @@ -130,9 +144,26 @@ pub async fn intiate_payment_link_flow( storage_enums::IntentStatus::RequiresCapture, storage_enums::IntentStatus::RequiresMerchantAction, storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::PartiallyCaptured, ], - ) || payment_link_status == api_models::payments::PaymentLinkStatus::Expired + ); + if is_terminal_state || payment_link_status == api_models::payments::PaymentLinkStatus::Expired { + let status = match payment_link_status { + api_models::payments::PaymentLinkStatus::Active => { + PaymentLinkStatusWrap::IntentStatus(payment_intent.status) + } + api_models::payments::PaymentLinkStatus::Expired => { + if is_terminal_state { + PaymentLinkStatusWrap::IntentStatus(payment_intent.status) + } else { + PaymentLinkStatusWrap::PaymentLinkStatus( + api_models::payments::PaymentLinkStatus::Expired, + ) + } + } + }; + let attempt_id = payment_intent.active_attempt.get_id().clone(); let payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -148,12 +179,14 @@ pub async fn intiate_payment_link_flow( currency, payment_id: payment_intent.payment_id, merchant_name, - merchant_logo: payment_link_config.clone().logo, + merchant_logo: payment_link_config.logo.clone(), created: payment_link.created_at, - intent_status: payment_intent.status, - payment_link_status, + status, error_code: payment_attempt.error_code, error_message: payment_attempt.error_message, + redirect: false, + theme: payment_link_config.theme.clone(), + return_url: return_url.clone(), }; let js_script = get_js_script( api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details), @@ -177,11 +210,11 @@ pub async fn intiate_payment_link_flow( session_expiry, pub_key, client_secret, - merchant_logo: payment_link_config.clone().logo, + merchant_logo: payment_link_config.logo.clone(), max_items_visible_after_collapse: 3, - theme: payment_link_config.clone().theme, + theme: payment_link_config.theme.clone(), merchant_description: payment_intent.description, - sdk_layout: payment_link_config.clone().sdk_layout, + sdk_layout: payment_link_config.sdk_layout.clone(), }; let js_script = get_js_script(api_models::payments::PaymentLinkData::PaymentLinkDetails( @@ -425,3 +458,123 @@ fn check_payment_link_invalid_conditions( ) -> bool { not_allowed_statuses.contains(intent_status) } + +pub async fn get_payment_link_status( + state: AppState, + merchant_account: domain::MerchantAccount, + merchant_id: String, + payment_id: String, +) -> RouterResponse { + let db = &*state.store; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + &merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_id, + &attempt_id.clone(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_link_id = payment_intent + .payment_link_id + .get_required_value("payment_link_id") + .change_context(errors::ApiErrorResponse::PaymentLinkNotFound)?; + + let merchant_name_from_merchant_account = merchant_account + .merchant_name + .clone() + .map(|merchant_name| merchant_name.into_inner().peek().to_owned()) + .unwrap_or_default(); + + let payment_link = db + .find_payment_link_by_payment_link_id(&payment_link_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; + + let payment_link_config = if let Some(pl_config_value) = payment_link.payment_link_config { + extract_payment_link_config(pl_config_value)? + } else { + admin_types::PaymentLinkConfig { + theme: DEFAULT_BACKGROUND_COLOR.to_string(), + logo: DEFAULT_MERCHANT_LOGO.to_string(), + seller_name: merchant_name_from_merchant_account, + sdk_layout: DEFAULT_SDK_LAYOUT.to_owned(), + } + }; + + let currency = + payment_intent + .currency + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "currency", + })?; + + let amount = currency + .to_currency_base_unit(payment_attempt.net_amount) + .into_report() + .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?; + + // converting first letter of merchant name to upperCase + let merchant_name = capitalize_first_char(&payment_link_config.seller_name); + let css_script = get_color_scheme_css(payment_link_config.clone()); + + let profile_id = payment_link + .profile_id + .or(payment_intent.profile_id) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Profile id missing in payment link and payment intent")?; + + let business_profile = db + .find_business_profile_by_profile_id(&profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let return_url = if let Some(payment_create_return_url) = payment_intent.return_url.clone() { + payment_create_return_url + } else { + business_profile + .return_url + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "return_url", + })? + }; + + let payment_details = api_models::payments::PaymentLinkStatusDetails { + amount, + currency, + payment_id: payment_intent.payment_id, + merchant_name, + merchant_logo: payment_link_config.logo.clone(), + created: payment_link.created_at, + status: PaymentLinkStatusWrap::IntentStatus(payment_intent.status), + error_code: payment_attempt.error_code, + error_message: payment_attempt.error_message, + redirect: true, + theme: payment_link_config.theme.clone(), + return_url, + }; + let js_script = get_js_script( + api_models::payments::PaymentLinkData::PaymentLinkStatusDetails(payment_details), + )?; + let payment_link_status_data = services::PaymentLinkStatusData { + js_script, + css_script, + }; + Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + services::api::PaymentLinkAction::PaymentLinkStatus(payment_link_status_data), + ))) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html deleted file mode 100644 index f6e62f8bdc8a..000000000000 --- a/crates/router/src/core/payment_link/payment_link.html +++ /dev/null @@ -1,2130 +0,0 @@ - - - - - - Payments requested by HyperSwitch - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-
-
-
-
-
-
-
- - - -
-
-
- -
-
-
-
- - - - Your Cart - - - -
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
-
-
- - - - {{ hyperloader_sdk_link }} - - diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css new file mode 100644 index 000000000000..ee5600e42bfe --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.css @@ -0,0 +1,785 @@ +{{ css_color_scheme }} + +html, +body { + height: 100%; + overflow: hidden; +} + +body { + display: flex; + flex-flow: column; + align-items: center; + justify-content: flex-start; + margin: 0; + color: #333333; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.hide-scrollbar { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +/* For ellipsis on text lines */ +.ellipsis-container-3 { + height: 4em; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + white-space: normal; +} + +.hidden { + display: none !important; +} + +.hyper-checkout { + display: flex; + background-color: #f8f9fb; + color: #333333; + width: 100%; + height: 100%; + overflow: scroll; +} + +#hyper-footer { + width: 100vw; + display: flex; + justify-content: center; + padding: 20px 0; +} + +.main { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + min-width: 600px; + width: 50vw; +} + +#hyper-checkout-details { + font-family: "Montserrat"; +} + +.hyper-checkout-payment { + min-width: 600px; + box-shadow: 0px 0px 5px #d1d1d1; + border-radius: 8px; + background-color: #fefefe; +} + +.hyper-checkout-payment-content-details { + display: flex; + flex-flow: column; + justify-content: space-between; + align-content: space-between; +} + +.content-details-wrap { + display: flex; + flex-flow: row; + margin: 20px 20px 30px 20px; + justify-content: space-between; +} + +.hyper-checkout-payment-price { + font-weight: 700; + font-size: 40px; + height: 64px; + display: flex; + align-items: center; +} + +#hyper-checkout-payment-merchant-details { + margin-top: 5px; +} + +.hyper-checkout-payment-merchant-name { + font-weight: 600; + font-size: 19px; +} + +.hyper-checkout-payment-ref { + font-size: 12px; + margin-top: 5px; +} + +.hyper-checkout-image-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +#hyper-checkout-merchant-image, +#hyper-checkout-cart-image { + height: 64px; + width: 64px; + border-radius: 4px; + display: flex; + align-self: flex-start; + align-items: center; + justify-content: center; +} + +#hyper-checkout-merchant-image > img { + height: 48px; + width: 48px; +} + +#hyper-checkout-cart-image { + display: none; + cursor: pointer; + height: 60px; + width: 60px; + border-radius: 100px; + background-color: #f5f5f5; +} + +#hyper-checkout-payment-footer { + margin-top: 20px; + background-color: #f5f5f5; + font-size: 13px; + font-weight: 500; + padding: 12px 20px; + border-radius: 0 0 8px 8px; +} + +#hyper-checkout-cart { + display: flex; + flex-flow: column; + min-width: 600px; + margin-top: 40px; + max-height: 60vh; +} + +#hyper-checkout-cart-items { + max-height: 291px; + overflow: scroll; + transition: all 0.3s ease; +} + +.hyper-checkout-cart-header { + font-size: 15px; + display: flex; + flex-flow: row; + align-items: center; +} + +.hyper-checkout-cart-header > span { + margin-left: 5px; + font-weight: 500; +} + +.cart-close { + display: none; + cursor: pointer; +} + +.hyper-checkout-cart-item { + display: flex; + flex-flow: row; + padding: 20px 0; + font-size: 15px; +} + +.hyper-checkout-cart-product-image { + height: 56px; + width: 56px; + border-radius: 4px; +} + +.hyper-checkout-card-item-name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.hyper-checkout-card-item-quantity { + border: 1px solid #e6e6e6; + border-radius: 3px; + width: max-content; + padding: 5px 12px; + background-color: #fafafa; + font-size: 13px; + font-weight: 500; +} + +.hyper-checkout-cart-product-details { + display: flex; + flex-flow: column; + margin-left: 15px; + justify-content: space-between; + width: 100%; +} + +.hyper-checkout-card-item-price { + justify-self: flex-end; + font-weight: 600; + font-size: 16px; + padding-left: 30px; + text-align: end; + min-width: max-content; +} + +.hyper-checkout-cart-item-divider { + height: 1px; + background-color: #e6e6e6; +} + +.hyper-checkout-cart-button { + font-size: 12px; + font-weight: 500; + cursor: pointer; + align-self: flex-start; + display: flex; + align-content: flex-end; + gap: 3px; + text-decoration: none; + transition: text-decoration 0.3s; + margin-top: 10px; +} + +.hyper-checkout-cart-button:hover { + text-decoration: underline; +} + +#hyper-checkout-merchant-description { + font-size: 13px; + color: #808080; +} + +.powered-by-hyper { + margin-top: 40px; + align-self: flex-start; +} + +.hyper-checkout-sdk { + width: 50vw; + min-width: 584px; + z-index: 2; + background-color: var(--primary-color); + box-shadow: 0px 1px 10px #f2f2f2; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; +} + +#payment-form-wrap { + min-width: 300px; + width: 30vw; + padding: 20px; + background-color: white; + border-radius: 3px; +} + +#hyper-checkout-sdk-header { + padding: 10px 10px 10px 22px; + display: flex; + align-items: flex-start; + justify-content: flex-start; + border-bottom: 1px solid #f2f2f2; +} + +.hyper-checkout-sdk-header-logo { + height: 60px; + width: 60px; + background-color: white; + border-radius: 2px; +} + +.hyper-checkout-sdk-header-logo > img { + height: 56px; + width: 56px; + margin: 2px; +} + +.hyper-checkout-sdk-header-items { + display: flex; + flex-flow: column; + color: white; + font-size: 20px; + font-weight: 700; +} + +.hyper-checkout-sdk-items { + margin-left: 10px; +} + +.hyper-checkout-sdk-header-brand-name, +.hyper-checkout-sdk-header-amount { + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + font-family: "Montserrat"; + justify-self: flex-start; +} + +.hyper-checkout-sdk-header-amount { + font-weight: 800; + font-size: 25px; +} + +.page-spinner { + position: absolute; + width: 100vw; + height: 100vh; + z-index: 3; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; +} + +.sdk-spinner { + width: 100%; + height: 100%; + z-index: 3; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + width: 60px; + height: 60px; +} + +.spinner div { + transform-origin: 30px 30px; + animation: spinner 1.2s linear infinite; +} + +.spinner div:after { + content: " "; + display: block; + position: absolute; + top: 3px; + left: 28px; + width: 4px; + height: 15px; + border-radius: 20%; + background: var(--primary-color); +} + +.spinner div:nth-child(1) { + transform: rotate(0deg); + animation-delay: -1.1s; +} + +.spinner div:nth-child(2) { + transform: rotate(30deg); + animation-delay: -1s; +} + +.spinner div:nth-child(3) { + transform: rotate(60deg); + animation-delay: -0.9s; +} + +.spinner div:nth-child(4) { + transform: rotate(90deg); + animation-delay: -0.8s; +} + +.spinner div:nth-child(5) { + transform: rotate(120deg); + animation-delay: -0.7s; +} + +.spinner div:nth-child(6) { + transform: rotate(150deg); + animation-delay: -0.6s; +} + +.spinner div:nth-child(7) { + transform: rotate(180deg); + animation-delay: -0.5s; +} + +.spinner div:nth-child(8) { + transform: rotate(210deg); + animation-delay: -0.4s; +} + +.spinner div:nth-child(9) { + transform: rotate(240deg); + animation-delay: -0.3s; +} + +.spinner div:nth-child(10) { + transform: rotate(270deg); + animation-delay: -0.2s; +} + +.spinner div:nth-child(11) { + transform: rotate(300deg); + animation-delay: -0.1s; +} + +.spinner div:nth-child(12) { + transform: rotate(330deg); + animation-delay: 0s; +} + +@keyframes spinner { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +#hyper-checkout-status-canvas { + width: 100%; + height: 100%; + justify-content: center; + align-items: center; + background-color: var(--primary-color); +} + +.hyper-checkout-status-wrap { + display: flex; + flex-flow: column; + font-family: "Montserrat"; + width: auto; + min-width: 400px; + background-color: white; + border-radius: 5px; +} + +#hyper-checkout-status-header { + max-width: 1200px; + border-radius: 3px; + border-bottom: 1px solid #e6e6e6; +} + +#hyper-checkout-status-header, +#hyper-checkout-status-content { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 24px; + font-weight: 600; + padding: 15px 20px; +} + +.hyper-checkout-status-amount { + font-family: "Montserrat"; + font-size: 35px; + font-weight: 700; +} + +.hyper-checkout-status-merchant-logo { + border: 1px solid #e6e6e6; + border-radius: 5px; + padding: 9px; + height: 48px; + width: 48px; +} + +#hyper-checkout-status-content { + height: 100%; + flex-flow: column; + min-height: 500px; + align-items: center; + justify-content: center; +} + +.hyper-checkout-status-image { + height: 200px; + width: 200px; +} + +.hyper-checkout-status-text { + text-align: center; + font-size: 21px; + font-weight: 600; + margin-top: 20px; +} + +.hyper-checkout-status-message { + text-align: center; + font-size: 12px !important; + margin-top: 10px; + font-size: 14px; + font-weight: 500; + max-width: 400px; +} + +.hyper-checkout-status-details { + display: flex; + flex-flow: column; + margin-top: 20px; + border-radius: 3px; + border: 1px solid #e6e6e6; + max-width: calc(100vw - 40px); +} + +.hyper-checkout-status-item { + display: flex; + align-items: center; + padding: 5px 10px; + border-bottom: 1px solid #e6e6e6; + word-wrap: break-word; +} + +.hyper-checkout-status-item:last-child { + border-bottom: 0; +} + +.hyper-checkout-item-header { + min-width: 13ch; + font-size: 12px; +} + +.hyper-checkout-item-value { + font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; + font-weight: 400; +} + +#hyper-checkout-status-redirect-message { + margin-top: 20px; + font-family: "Montserrat"; + font-size: 13px; +} + +@keyframes loading { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes slide-from-right { + from { + right: -582px; + } + + to { + right: 0; + } +} + +@keyframes slide-to-right { + from { + right: 0; + } + + to { + right: -582px; + } +} + +#payment-message { + font-size: 12px; + font-weight: 500; + padding: 2%; + color: #ff0000; + font-family: "Montserrat"; +} + +#payment-form { + max-width: 560px; + width: 100%; + min-height: 500px; + max-height: 90vh; + height: 100%; + overflow: scroll; + margin: 0 auto; + text-align: center; +} + +#submit { + cursor: pointer; + margin-top: 20px; + width: 100%; + height: 38px; + background-color: var(--primary-color); + border: 0; + border-radius: 4px; + font-size: 18px; + display: flex; + justify-content: center; + align-items: center; +} + +#submit.disabled { + cursor: not-allowed; +} + +#submit-spinner { + width: 28px; + height: 28px; + border: 4px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: loading 1s linear infinite; +} + +@media only screen and (max-width: 1400px) { + body { + overflow-y: scroll; + } + + .hyper-checkout { + flex-flow: column; + margin: 0; + height: auto; + overflow: visible; + } + + #hyper-checkout-payment-merchant-details { + margin-top: 20px; + } + + .main { + width: auto; + min-width: 300px; + } + + .hyper-checkout-payment { + min-width: 300px; + width: calc(100vw - 50px); + margin: 0; + padding: 25px; + border: 0; + border-radius: 0; + background-color: var(--primary-color); + display: flex; + flex-flow: column; + justify-self: flex-start; + align-self: flex-start; + } + + .hyper-checkout-payment-content-details { + max-width: 520px; + width: 100%; + align-self: center; + margin-bottom: 0; + } + + .content-details-wrap { + flex-flow: column; + flex-direction: column-reverse; + margin: 0; + } + + #hyper-checkout-merchant-image { + background-color: white; + } + + #hyper-checkout-cart-image { + display: flex; + } + + .hyper-checkout-payment-price { + font-size: 48px; + margin-top: 20px; + } + + .hyper-checkout-payment-merchant-name { + font-size: 18px; + } + + #hyper-checkout-payment-footer { + border-radius: 50px; + width: max-content; + padding: 10px 20px; + } + + #hyper-checkout-cart { + position: absolute; + top: 0; + right: 0; + z-index: 100; + margin: 0; + min-width: 300px; + max-width: 582px; + max-height: 100vh; + width: 100vw; + height: 100vh; + background-color: #f5f5f5; + box-shadow: 0px 10px 10px #aeaeae; + right: 0px; + animation: slide-from-right 0.3s linear; + } + + .hyper-checkout-cart-header { + margin: 10px 0 0 10px; + } + + .cart-close { + margin: 0 10px 0 auto; + display: inline; + } + + #hyper-checkout-cart-items { + margin: 20px 20px 0 20px; + padding: 0; + } + + .hyper-checkout-cart-button { + margin: 10px; + text-align: right; + } + + .powered-by-hyper { + display: none; + } + + #hyper-checkout-sdk { + background-color: transparent; + width: auto; + min-width: 300px; + box-shadow: none; + } + + #payment-form-wrap { + min-width: 300px; + width: calc(100vw - 40px); + margin: 0; + padding: 25px 20px; + } + + #hyper-checkout-status-canvas { + background-color: #fefefe; + } + + .hyper-checkout-status-wrap { + min-width: 100vw; + width: 100vw; + } + + #hyper-checkout-status-header { + max-width: calc(100% - 40px); + } +} diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html new file mode 100644 index 000000000000..be4369be1d97 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.html @@ -0,0 +1,197 @@ + + + + + + Payments requested by HyperSwitch + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+ + + + Your Cart + + + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ + + {{ hyperloader_sdk_link }} + + diff --git a/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js new file mode 100644 index 000000000000..75e063c3a991 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js @@ -0,0 +1,906 @@ +// @ts-check + +/** + * UTIL FUNCTIONS + */ + +function adjustLightness(hexColor, factor) { + // Convert hex to RGB + var r = parseInt(hexColor.slice(1, 3), 16); + var g = parseInt(hexColor.slice(3, 5), 16); + var b = parseInt(hexColor.slice(5, 7), 16); + + // Convert RGB to HSL + var hsl = rgbToHsl(r, g, b); + + // Adjust lightness + hsl[2] = Math.max(0, Math.min(100, hsl[2] * factor)); + + // Convert HSL back to RGB + var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); + + // Convert RGB to hex + var newHexColor = rgbToHex(rgb[0], rgb[1], rgb[2]); + + return newHexColor; +} +function rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var max = Math.max(r, g, b), + min = Math.min(r, g, b); + var h = 1, + s, + l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [h * 360, s * 100, l * 100]; +} +function hslToRgb(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + var r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + var hue2rgb = function (p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [r * 255, g * 255, b * 255]; +} +function rgbToHex(r, g, b) { + var toHex = function (c) { + var hex = Math.round(c).toString(16); + return hex.length === 1 ? "0" + hex : hex; + }; + return "#" + toHex(r) + toHex(g) + toHex(b); +} + +/** + * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js + */ +function padz(str, len) { + if (len === void 0) { + len = 2; + } + return (new Array(len).join("0") + str).slice(-len); +} +function hexToRgbArray(hex) { + if (hex.slice(0, 1) === "#") hex = hex.slice(1); + var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i; + if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"'); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + return [ + parseInt(hex.slice(0, 2), 16), + parseInt(hex.slice(2, 4), 16), + parseInt(hex.slice(4, 6), 16), + ]; +} +function toRgbArray(c) { + if (!c) throw new Error("Invalid color value"); + if (Array.isArray(c)) return c; + return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b]; +} +function getLuminance(c) { + var i, x; + var a = []; + for (i = 0; i < c.length; i++) { + x = c[i] / 255; + a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + } + return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; +} +function invertToBW(color, bw, asArr) { + var DEFAULT_BW = { + black: "#090302", + white: "#FFFFFC", + threshold: Math.sqrt(1.05 * 0.05) - 0.05, + }; + var options = bw === true ? DEFAULT_BW : Object.assign({}, DEFAULT_BW, bw); + return getLuminance(color) > options.threshold + ? asArr + ? hexToRgbArray(options.black) + : options.black + : asArr + ? hexToRgbArray(options.white) + : options.white; +} +function invert(color, bw) { + if (bw === void 0) { + bw = false; + } + color = toRgbArray(color); + if (bw) return invertToBW(color, bw); + return ( + "#" + + color + .map(function (c) { + return padz((255 - c).toString(16)); + }) + .join("") + ); +} + +/** + * UTIL FUNCTIONS END HERE + */ + +{{ payment_details_js_script }} + +// @ts-ignore +window.state = { + prevHeight: window.innerHeight, + prevWidth: window.innerWidth, + isMobileView: window.innerWidth <= 1400, + currentScreen: "payment_link", +}; + +var widgets = null; +var unifiedCheckout = null; +// @ts-ignore +var pub_key = window.__PAYMENT_DETAILS.pub_key; +var hyper = null; + +/** + * Trigger - init function invoked once the script tag is loaded + * Use + * - Update document's title + * - Update document's icon + * - Render and populate document with payment details and cart + * - Initialize event listeners for updating UI on screen size changes + * - Initialize SDK + **/ +function boot() { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + + if (paymentDetails.merchant_name) { + document.title = "Payment requested by " + paymentDetails.merchant_name; + } + + if (paymentDetails.merchant_logo) { + var link = document.createElement("link"); + link.rel = "icon"; + link.href = paymentDetails.merchant_logo; + link.type = "image/x-icon"; + document.head.appendChild(link); + } + + // Render UI + renderPaymentDetails(paymentDetails); + renderSDKHeader(paymentDetails); + renderCart(paymentDetails); + + // Deal w loaders + show("#sdk-spinner"); + hide("#page-spinner"); + hide("#unified-checkout"); + + // Add event listeners + initializeEventListeners(paymentDetails); + + // Initialize SDK + // @ts-ignore + if (window.Hyper) { + initializeSDK(); + } + + // State specific functions + // @ts-ignore + if (window.state.isMobileView) { + show("#hyper-footer"); + hide("#hyper-checkout-cart"); + } else { + show("#hyper-checkout-cart"); + } +} +boot(); + +/** + * Use - add event listeners for changing UI on screen resize + * @param {PaymentDetails} paymentDetails + */ +function initializeEventListeners(paymentDetails) { + var primaryColor = paymentDetails.theme; + var lighterColor = adjustLightness(primaryColor, 1.4); + var darkerColor = adjustLightness(primaryColor, 0.8); + var contrastBWColor = invert(primaryColor, true); + var a = lighterColor.match(/[fF]/gi); + var contrastingTone = + Array.isArray(a) && a.length > 4 ? darkerColor : lighterColor; + var hyperCheckoutNode = document.getElementById("hyper-checkout-payment"); + var hyperCheckoutCartImageNode = document.getElementById( + "hyper-checkout-cart-image" + ); + var hyperCheckoutFooterNode = document.getElementById( + "hyper-checkout-payment-footer" + ); + var submitButtonNode = document.getElementById("submit"); + var submitButtonLoaderNode = document.getElementById("submit-spinner"); + + if (submitButtonLoaderNode instanceof HTMLSpanElement) { + submitButtonLoaderNode.style.borderBottomColor = contrastingTone; + } + + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.style.color = contrastBWColor; + } + + if (hyperCheckoutCartImageNode instanceof HTMLDivElement) { + hyperCheckoutCartImageNode.style.backgroundColor = contrastingTone; + } + + if (window.innerWidth <= 1400) { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = contrastBWColor; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = contrastingTone; + } + } else if (window.innerWidth > 1400) { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = "#333333"; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; + } + } + + window.addEventListener("resize", function (event) { + var currentHeight = window.innerHeight; + var currentWidth = window.innerWidth; + // @ts-ignore + if (currentWidth <= 1400 && window.state.prevWidth > 1400) { + hide("#hyper-checkout-cart"); + // @ts-ignore + if (window.state.currentScreen === "payment_link") { + show("#hyper-footer"); + } + try { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = contrastBWColor; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = lighterColor; + } + } catch (error) { + console.error("Failed to fetch primary-color, using default", error); + } + // @ts-ignore + } else if (currentWidth > 1400 && window.state.prevWidth <= 1400) { + // @ts-ignore + if (window.state.currentScreen === "payment_link") { + hide("#hyper-footer"); + } + show("#hyper-checkout-cart"); + try { + if (hyperCheckoutNode instanceof HTMLDivElement) { + hyperCheckoutNode.style.color = "#333333"; + } + if (hyperCheckoutFooterNode instanceof HTMLDivElement) { + hyperCheckoutFooterNode.style.backgroundColor = "#F5F5F5"; + } + } catch (error) { + console.error("Failed to revert back to default colors", error); + } + } + + // @ts-ignore + window.state.prevHeight = currentHeight; + // @ts-ignore + window.state.prevWidth = currentWidth; + // @ts-ignore + window.state.isMobileView = currentWidth <= 1400; + }); +} + +/** + * Trigger - post mounting SDK + * Use - set relevant classes to elements in the doc for showing SDK + **/ +function showSDK() { + show("#hyper-checkout-sdk"); + show("#hyper-checkout-details"); + show("#submit"); + show("#unified-checkout"); + hide("#sdk-spinner"); +} + +/** + * Trigger - post downloading SDK + * Uses + * - Instantiate SDK + * - Create a payment widget + * - Decide whether or not to show SDK (based on status) + **/ +function initializeSDK() { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + var client_secret = paymentDetails.client_secret; + var appearance = { + variables: { + colorPrimary: paymentDetails.theme || "rgb(0, 109, 249)", + fontFamily: "Work Sans, sans-serif", + fontSizeBase: "16px", + colorText: "rgb(51, 65, 85)", + colorTextSecondary: "#334155B3", + colorPrimaryText: "rgb(51, 65, 85)", + colorTextPlaceholder: "#33415550", + borderColor: "#33415550", + colorBackground: "rgb(255, 255, 255)", + }, + }; + // @ts-ignore + hyper = window.Hyper(pub_key, { + isPreloadEnabled: false, + }); + widgets = hyper.widgets({ + appearance: appearance, + clientSecret: client_secret, + }); + var type = + paymentDetails.sdk_layout === "spaced_accordion" || + paymentDetails.sdk_layout === "accordion" + ? "accordion" + : paymentDetails.sdk_layout; + + var unifiedCheckoutOptions = { + disableSaveCards: true, + layout: { + type: type, //accordion , tabs, spaced accordion + spacedAccordionItems: paymentDetails.sdk_layout === "spaced_accordion", + }, + branding: "never", + wallets: { + walletReturnUrl: paymentDetails.return_url, + style: { + theme: "dark", + type: "default", + height: 55, + }, + }, + }; + unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); + mountUnifiedCheckout("#unified-checkout"); + showSDK(); +} + +/** + * Use - mount payment widget on the passed element + * @param {String} id + **/ +function mountUnifiedCheckout(id) { + if (unifiedCheckout !== null) { + unifiedCheckout.mount(id); + } +} + +/** + * Trigger - on clicking submit button + * Uses + * - Trigger /payment/confirm through SDK + * - Toggle UI loaders appropriately + * - Handle errors and redirect to status page + * @param {Event} e + */ +function handleSubmit(e) { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + + // Update button loader + hide("#submit-button-text"); + show("#submit-spinner"); + var submitButtonNode = document.getElementById("submit"); + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.disabled = true; + submitButtonNode.classList.add("disabled"); + } + + hyper + .confirmPayment({ + widgets: widgets, + confirmParams: { + // Make sure to change this to your payment completion page + return_url: paymentDetails.return_url, + }, + }) + .then(function (result) { + var error = result.error; + if (error) { + if (error.type === "validation_error") { + showMessage(error.message); + } else { + showMessage("An unexpected error occurred."); + } + } else { + redirectToStatus(); + } + }) + .catch(function (error) { + console.error("Error confirming payment_intent", error); + }) + .finally(() => { + hide("#submit-spinner"); + show("#submit-button-text"); + if (submitButtonNode instanceof HTMLButtonElement) { + submitButtonNode.disabled = false; + submitButtonNode.classList.remove("disabled"); + } + }); +} + +function show(id) { + removeClass(id, "hidden"); +} +function hide(id) { + addClass(id, "hidden"); +} + +function showMessage(msg) { + show("#payment-message"); + addText("#payment-message", msg); +} + +/** + * Use - redirect to /payment_link/status + */ +function redirectToStatus() { + var arr = window.location.pathname.split("/"); + arr.splice(0, 2); + arr.unshift("status"); + arr.unshift("payment_link"); + window.location.href = window.location.origin + "/" + arr.join("/"); +} + +function addText(id, msg) { + var element = document.querySelector(id); + element.innerText = msg; +} + +function addClass(id, className) { + var element = document.querySelector(id); + element.classList.add(className); +} + +function removeClass(id, className) { + var element = document.querySelector(id); + element.classList.remove(className); +} + +/** + * Use - format date in "hh:mm AM/PM timezone MM DD, YYYY" + * @param {Date} date + **/ +function formatDate(date) { + var months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + var hours = date.getHours(); + var minutes = date.getMinutes(); + minutes = minutes < 10 ? "0" + minutes : minutes; + var suffix = hours > 11 ? "PM" : "AM"; + hours = hours % 12; + hours = hours ? hours : 12; + var day = date.getDate(); + var month = months[date.getMonth()]; + var year = date.getUTCFullYear(); + + // @ts-ignore + var locale = navigator.language || navigator.userLanguage; + var timezoneShorthand = date + .toLocaleDateString(locale, { + day: "2-digit", + timeZoneName: "long", + }) + .substring(4) + .split(" ") + .reduce(function (tz, c) { + return tz + c.charAt(0).toUpperCase(); + }, ""); + + var formatted = + hours + + ":" + + minutes + + " " + + suffix + + " " + + timezoneShorthand + + " " + + month + + " " + + day + + ", " + + year; + return formatted; +} + +/** + * Trigger - on boot + * Uses + * - Render payment related details (header bit) + * - Amount + * - Merchant's name + * - Expiry + * @param {PaymentDetails} paymentDetails + **/ +function renderPaymentDetails(paymentDetails) { + // Create price node + var priceNode = document.createElement("div"); + priceNode.className = "hyper-checkout-payment-price"; + priceNode.innerText = paymentDetails.currency + " " + paymentDetails.amount; + + // Create merchant name's node + var merchantNameNode = document.createElement("div"); + merchantNameNode.className = "hyper-checkout-payment-merchant-name"; + merchantNameNode.innerText = "Requested by " + paymentDetails.merchant_name; + + // Create payment ID node + var paymentIdNode = document.createElement("div"); + paymentIdNode.className = "hyper-checkout-payment-ref"; + paymentIdNode.innerText = "Ref Id: " + paymentDetails.payment_id; + + // Create merchant logo's node + var merchantLogoNode = document.createElement("img"); + merchantLogoNode.src = paymentDetails.merchant_logo; + + // Create expiry node + var paymentExpiryNode = document.createElement("div"); + paymentExpiryNode.className = "hyper-checkout-payment-footer-expiry"; + var expiryDate = new Date(paymentDetails.session_expiry); + var formattedDate = formatDate(expiryDate); + paymentExpiryNode.innerText = "Link expires on: " + formattedDate; + + // Append information to DOM + var paymentContextNode = document.getElementById( + "hyper-checkout-payment-context" + ); + if (paymentContextNode instanceof HTMLDivElement) { + paymentContextNode.prepend(priceNode); + } + var paymentMerchantDetails = document.getElementById( + "hyper-checkout-payment-merchant-details" + ); + if (paymentMerchantDetails instanceof HTMLDivElement) { + paymentMerchantDetails.append(merchantNameNode); + paymentMerchantDetails.append(paymentIdNode); + } + var merchantImageNode = document.getElementById( + "hyper-checkout-merchant-image" + ); + if (merchantImageNode instanceof HTMLDivElement) { + merchantImageNode.prepend(merchantLogoNode); + } + var footerNode = document.getElementById("hyper-checkout-payment-footer"); + if (footerNode instanceof HTMLDivElement) { + footerNode.append(paymentExpiryNode); + } +} + +/** + * Trigger - on boot + * Uses + * - Render cart wrapper and items + * - Attaches an onclick event for toggling expand on the items list + * @param {PaymentDetails} paymentDetails + **/ +function renderCart(paymentDetails) { + var orderDetails = paymentDetails.order_details; + + // Cart items + if (Array.isArray(orderDetails) && orderDetails.length > 0) { + var cartNode = document.getElementById("hyper-checkout-cart"); + var cartItemsNode = document.getElementById("hyper-checkout-cart-items"); + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + paymentDetails.max_items_visible_after_collapse; + + orderDetails.map(function (item, index) { + if (index >= MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + return; + } + renderCartItem( + item, + paymentDetails, + index !== 0 && index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE, + cartItemsNode + ); + }); + // Expand / collapse button + var totalItems = orderDetails.length; + if (totalItems > MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + var expandButtonNode = document.createElement("div"); + expandButtonNode.className = "hyper-checkout-cart-button"; + expandButtonNode.onclick = () => { + handleCartView(paymentDetails); + }; + var buttonImageNode = document.createElement("svg"); + buttonImageNode.id = "hyper-checkout-cart-button-arrow"; + var arrowDownImage = document.getElementById("arrow-down"); + if (arrowDownImage instanceof Object) { + buttonImageNode.innerHTML = arrowDownImage.innerHTML; + } + var buttonTextNode = document.createElement("span"); + buttonTextNode.id = "hyper-checkout-cart-button-text"; + var hiddenItemsCount = + orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; + buttonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; + expandButtonNode.append(buttonTextNode, buttonImageNode); + if (cartNode instanceof HTMLDivElement) { + cartNode.insertBefore(expandButtonNode, cartNode.lastElementChild); + } + } + } else { + hide("#hyper-checkout-cart-header"); + hide("#hyper-checkout-cart-items"); + hide("#hyper-checkout-cart-image"); + if ( + typeof paymentDetails.merchant_description === "string" && + paymentDetails.merchant_description.length > 0 + ) { + var merchantDescriptionNode = document.getElementById( + "hyper-checkout-merchant-description" + ); + if (merchantDescriptionNode instanceof HTMLDivElement) { + merchantDescriptionNode.innerText = paymentDetails.merchant_description; + } + show("#hyper-checkout-merchant-description"); + } + } +} + +/** + * Trigger - on cart render + * Uses + * - Renders a single cart item which includes + * - Product image + * - Product name + * - Quantity + * - Single item amount + * @param {OrderDetailsWithAmount} item + * @param {PaymentDetails} paymentDetails + * @param {boolean} shouldAddDividerNode + * @param {HTMLDivElement} cartItemsNode + **/ +function renderCartItem( + item, + paymentDetails, + shouldAddDividerNode, + cartItemsNode +) { + // Wrappers + var itemWrapperNode = document.createElement("div"); + itemWrapperNode.className = "hyper-checkout-cart-item"; + var nameAndQuantityWrapperNode = document.createElement("div"); + nameAndQuantityWrapperNode.className = "hyper-checkout-cart-product-details"; + // Image + var productImageNode = document.createElement("img"); + productImageNode.className = "hyper-checkout-cart-product-image"; + productImageNode.src = item.product_img_link; + // Product title + var productNameNode = document.createElement("div"); + productNameNode.className = "hyper-checkout-card-item-name"; + productNameNode.innerText = item.product_name; + // Product quantity + var quantityNode = document.createElement("div"); + quantityNode.className = "hyper-checkout-card-item-quantity"; + quantityNode.innerText = "Qty: " + item.quantity; + // Product price + var priceNode = document.createElement("div"); + priceNode.className = "hyper-checkout-card-item-price"; + priceNode.innerText = paymentDetails.currency + " " + item.amount; + // Append items + nameAndQuantityWrapperNode.append(productNameNode, quantityNode); + itemWrapperNode.append( + productImageNode, + nameAndQuantityWrapperNode, + priceNode + ); + if (shouldAddDividerNode) { + var dividerNode = document.createElement("div"); + dividerNode.className = "hyper-checkout-cart-item-divider"; + cartItemsNode.append(dividerNode); + } + cartItemsNode.append(itemWrapperNode); +} + +/** + * Trigger - on toggling expansion of cart list + * Uses + * - Render or delete items based on current state of the rendered cart list + * @param {PaymentDetails} paymentDetails + **/ +function handleCartView(paymentDetails) { + var orderDetails = paymentDetails.order_details; + var MAX_ITEMS_VISIBLE_AFTER_COLLAPSE = + paymentDetails.max_items_visible_after_collapse; + var itemsHTMLCollection = document.getElementsByClassName( + "hyper-checkout-cart-item" + ); + var dividerHTMLCollection = document.getElementsByClassName( + "hyper-checkout-cart-item-divider" + ); + var cartItems = [].slice.call(itemsHTMLCollection); + var dividerItems = [].slice.call(dividerHTMLCollection); + var isHidden = cartItems.length < orderDetails.length; + var cartItemsNode = document.getElementById("hyper-checkout-cart-items"); + var cartButtonTextNode = document.getElementById( + "hyper-checkout-cart-button-text" + ); + var cartButtonImageNode = document.getElementById( + "hyper-checkout-cart-button-arrow" + ); + if (isHidden) { + if (Array.isArray(orderDetails)) { + orderDetails.map(function (item, index) { + if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + return; + } + renderCartItem( + item, + paymentDetails, + index >= MAX_ITEMS_VISIBLE_AFTER_COLLAPSE, + cartItemsNode + ); + }); + } + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.style.maxHeight = cartItemsNode.scrollHeight + "px"; + cartItemsNode.style.height = cartItemsNode.scrollHeight + "px"; + } + if (cartButtonTextNode instanceof HTMLButtonElement) { + cartButtonTextNode.innerText = "Show Less"; + } + var arrowUpImage = document.getElementById("arrow-up"); + if ( + cartButtonImageNode instanceof Object && + arrowUpImage instanceof Object + ) { + cartButtonImageNode.innerHTML = arrowUpImage.innerHTML; + } + } else { + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.style.maxHeight = "300px"; + cartItemsNode.style.height = "290px"; + cartItemsNode.scrollTo({ top: 0, behavior: "smooth" }); + setTimeout(function () { + cartItems.map(function (item, index) { + if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE) { + return; + } + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.removeChild(item); + } + }); + dividerItems.map(function (item, index) { + if (index < MAX_ITEMS_VISIBLE_AFTER_COLLAPSE - 1) { + return; + } + if (cartItemsNode instanceof HTMLDivElement) { + cartItemsNode.removeChild(item); + } + }); + }, 300); + } + setTimeout(function () { + var hiddenItemsCount = + orderDetails.length - MAX_ITEMS_VISIBLE_AFTER_COLLAPSE; + if (cartButtonTextNode instanceof HTMLButtonElement) { + cartButtonTextNode.innerText = "Show More (" + hiddenItemsCount + ")"; + } + var arrowDownImage = document.getElementById("arrow-down"); + if ( + cartButtonImageNode instanceof Object && + arrowDownImage instanceof Object + ) { + cartButtonImageNode.innerHTML = arrowDownImage.innerHTML; + } + }, 250); + } +} + +/** + * Use - hide cart when in mobile view + **/ +function hideCartInMobileView() { + window.history.back(); + var cartNode = document.getElementById("hyper-checkout-cart"); + if (cartNode instanceof HTMLDivElement) { + cartNode.style.animation = "slide-to-right 0.3s linear"; + cartNode.style.right = "-582px"; + } + setTimeout(function () { + hide("#hyper-checkout-cart"); + }, 300); +} + +/** + * Use - show cart when in mobile view + **/ +function viewCartInMobileView() { + window.history.pushState("view-cart", ""); + var cartNode = document.getElementById("hyper-checkout-cart"); + if (cartNode instanceof HTMLDivElement) { + cartNode.style.animation = "slide-from-right 0.3s linear"; + cartNode.style.right = "0px"; + } + show("#hyper-checkout-cart"); +} + +/** + * Trigger - on boot + * Uses + * - Render SDK header node + * - merchant's name + * - currency + amount + * @param {PaymentDetails} paymentDetails + **/ +function renderSDKHeader(paymentDetails) { + // SDK headers' items + var sdkHeaderItemNode = document.createElement("div"); + sdkHeaderItemNode.className = "hyper-checkout-sdk-items"; + var sdkHeaderMerchantNameNode = document.createElement("div"); + sdkHeaderMerchantNameNode.className = "hyper-checkout-sdk-header-brand-name"; + sdkHeaderMerchantNameNode.innerText = paymentDetails.merchant_name; + var sdkHeaderAmountNode = document.createElement("div"); + sdkHeaderAmountNode.className = "hyper-checkout-sdk-header-amount"; + sdkHeaderAmountNode.innerText = + paymentDetails.currency + " " + paymentDetails.amount; + sdkHeaderItemNode.append(sdkHeaderMerchantNameNode); + sdkHeaderItemNode.append(sdkHeaderAmountNode); + + // Append to SDK header's node + var sdkHeaderNode = document.getElementById("hyper-checkout-sdk-header"); + if (sdkHeaderNode instanceof HTMLDivElement) { + // sdkHeaderNode.append(sdkHeaderLogoNode); + sdkHeaderNode.append(sdkHeaderItemNode); + } +} diff --git a/crates/router/src/core/payment_link/payment_link_status/status.css b/crates/router/src/core/payment_link/payment_link_status/status.css new file mode 100644 index 000000000000..113f13531381 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_status/status.css @@ -0,0 +1,161 @@ +{{ css_color_scheme }} + +body, +body > div { + height: 100vh; + width: 100vw; +} + +body { + font-family: "Montserrat"; + background-color: var(--primary-color); + color: #333; + text-align: center; + margin: 0; + padding: 0; + overflow: hidden; +} + +body > div { + height: 100vh; + width: 100vw; + overflow: scroll; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; +} + +.hyper-checkout-status-wrap { + display: flex; + flex-flow: column; + font-family: "Montserrat"; + width: auto; + min-width: 400px; + max-width: 800px; + background-color: white; + border-radius: 5px; +} + +#hyper-checkout-status-header { + max-width: 1200px; + border-radius: 3px; + border-bottom: 1px solid #e6e6e6; +} + +#hyper-checkout-status-header, +#hyper-checkout-status-content { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 24px; + font-weight: 600; + padding: 15px 20px; +} + +.hyper-checkout-status-amount { + font-family: "Montserrat"; + font-size: 35px; + font-weight: 700; +} + +.hyper-checkout-status-merchant-logo { + border: 1px solid #e6e6e6; + border-radius: 5px; + padding: 9px; + height: 48px; + width: 48px; +} + +#hyper-checkout-status-content { + height: 100%; + flex-flow: column; + min-height: 500px; + align-items: center; + justify-content: center; +} + +.hyper-checkout-status-image { + height: 200px; + width: 200px; +} + +.hyper-checkout-status-text { + text-align: center; + font-size: 21px; + font-weight: 600; + margin-top: 20px; +} + +.hyper-checkout-status-message { + text-align: center; + font-size: 12px !important; + margin-top: 10px; + font-size: 14px; + font-weight: 500; + max-width: 400px; +} + +.hyper-checkout-status-details { + display: flex; + flex-flow: column; + margin-top: 20px; + border-radius: 3px; + border: 1px solid #e6e6e6; + max-width: calc(100vw - 40px); +} + +.hyper-checkout-status-item { + display: flex; + align-items: center; + padding: 5px 10px; + border-bottom: 1px solid #e6e6e6; + word-wrap: break-word; +} + +.hyper-checkout-status-item:last-child { + border-bottom: 0; +} + +.hyper-checkout-item-header { + min-width: 13ch; + font-size: 12px; +} + +.hyper-checkout-item-value { + font-size: 12px; + overflow-x: hidden; + overflow-y: auto; + word-wrap: break-word; + font-weight: 400; + text-align: center; +} + +#hyper-checkout-status-redirect-message { + margin-top: 20px; + font-family: "Montserrat"; + font-size: 13px; +} + +.ellipsis-container-2 { + height: 2.5em; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + white-space: normal; +} + +@media only screen and (max-width: 1136px) { + .info { + flex-flow: column; + align-self: flex-start; + align-items: flex-start; + min-width: auto; + } + + .value { + margin: 0; + } +} \ No newline at end of file diff --git a/crates/router/src/core/payment_link/payment_link_status/status.html b/crates/router/src/core/payment_link/payment_link_status/status.html new file mode 100644 index 000000000000..00f1efe38c01 --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_status/status.html @@ -0,0 +1,26 @@ + + + + + Payment Status + + + + + +
+
+
+
+
+
+
+ + diff --git a/crates/router/src/core/payment_link/payment_link_status/status.js b/crates/router/src/core/payment_link/payment_link_status/status.js new file mode 100644 index 000000000000..7bf2dbaabb3b --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link_status/status.js @@ -0,0 +1,379 @@ +// @ts-check + +/** + * UTIL FUNCTIONS + */ + +/** + * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js + */ +function padz(str, len) { + if (len === void 0) { + len = 2; + } + return (new Array(len).join("0") + str).slice(-len); +} +function hexToRgbArray(hex) { + if (hex.slice(0, 1) === "#") hex = hex.slice(1); + var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i; + if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"'); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + return [ + parseInt(hex.slice(0, 2), 16), + parseInt(hex.slice(2, 4), 16), + parseInt(hex.slice(4, 6), 16), + ]; +} +function toRgbArray(c) { + if (!c) throw new Error("Invalid color value"); + if (Array.isArray(c)) return c; + return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b]; +} +function getLuminance(c) { + var i, x; + var a = []; + for (i = 0; i < c.length; i++) { + x = c[i] / 255; + a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + } + return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; +} +function invertToBW(color, bw, asArr) { + var DEFAULT_BW = { + black: "#090302", + white: "#FFFFFC", + threshold: Math.sqrt(1.05 * 0.05) - 0.05, + }; + var options = bw === true ? DEFAULT_BW : Object.assign({}, DEFAULT_BW, bw); + return getLuminance(color) > options.threshold + ? asArr + ? hexToRgbArray(options.black) + : options.black + : asArr + ? hexToRgbArray(options.white) + : options.white; +} +function invert(color, bw) { + if (bw === void 0) { + bw = false; + } + color = toRgbArray(color); + if (bw) return invertToBW(color, bw); + return ( + "#" + + color + .map(function (c) { + return padz((255 - c).toString(16)); + }) + .join("") + ); +} + +/** + * UTIL FUNCTIONS END HERE + */ + +// @ts-ignore +{{ payment_details_js_script }} + +// @ts-ignore +window.state = { + prevHeight: window.innerHeight, + prevWidth: window.innerWidth, + isMobileView: window.innerWidth <= 1400, +}; + +/** + * Trigger - init function invoked once the script tag is loaded + * Use + * - Update document's title + * - Update document's icon + * - Render and populate document with payment details and cart + * - Initialize event listeners for updating UI on screen size changes + * - Initialize SDK + **/ +function boot() { + // @ts-ignore + var paymentDetails = window.__PAYMENT_DETAILS; + + // Attach document icon + if (paymentDetails.merchant_logo) { + var link = document.createElement("link"); + link.rel = "icon"; + link.href = paymentDetails.merchant_logo; + link.type = "image/x-icon"; + document.head.appendChild(link); + } + + // Render status details + renderStatusDetails(paymentDetails); + + // Add event listeners + initializeEventListeners(paymentDetails); +} + +/** + * Trigger - on boot + * Uses + * - Render status details + * - Header - (amount, merchant name, merchant logo) + * - Body - status with image + * - Footer - payment details (id | error code and msg, if any) + * @param {PaymentDetails} paymentDetails + **/ +function renderStatusDetails(paymentDetails) { + var status = paymentDetails.status; + + var statusDetails = { + imageSource: "", + message: "", + status: status, + amountText: "", + items: [], + }; + + // Payment details + var paymentId = createItem("Ref Id", paymentDetails.payment_id); + // @ts-ignore + statusDetails.items.push(paymentId); + + // Status specific information + switch (status) { + case "expired": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Payment Link Expired"; + statusDetails.message = + "Sorry, this payment link has expired. Please use below reference for further investigation."; + break; + + case "succeeded": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png"; + statusDetails.message = "We have successfully received your payment"; + statusDetails.status = "Paid successfully"; + statusDetails.amountText = new Date( + paymentDetails.created + ).toTimeString(); + break; + + case "processing": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.png"; + statusDetails.message = + "Sorry! Your payment is taking longer than expected. Please check back again in sometime."; + statusDetails.status = "Payment Pending"; + break; + + case "failed": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Payment Failed!"; + var errorCodeNode = createItem("Error code", paymentDetails.error_code); + var errorMessageNode = createItem( + "Error message", + paymentDetails.error_message + ); + // @ts-ignore + statusDetails.items.push(errorMessageNode, errorCodeNode); + break; + + case "cancelled": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Payment Cancelled"; + break; + + case "requires_merchant_action": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/pending.png"; + statusDetails.status = "Payment under review"; + break; + + case "requires_capture": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png"; + statusDetails.message = "We have successfully received your payment"; + statusDetails.status = "Payment Success"; + break; + + case "partially_captured": + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/success.png"; + statusDetails.message = "Partial payment was captured."; + statusDetails.status = "Payment Success"; + break; + + default: + statusDetails.imageSource = "https://live.hyperswitch.io/payment-link-assets/failed.png"; + statusDetails.status = "Something went wrong"; + // Error details + if (typeof paymentDetails.error === "object") { + var errorCodeNode = createItem("Error Code", paymentDetails.error.code); + var errorMessageNode = createItem( + "Error Message", + paymentDetails.error.message + ); + // @ts-ignore + statusDetails.items.push(errorMessageNode, errorCodeNode); + } + break; + } + + // Form header items + var amountNode = document.createElement("div"); + amountNode.className = "hyper-checkout-status-amount"; + amountNode.innerText = paymentDetails.currency + " " + paymentDetails.amount; + var merchantLogoNode = document.createElement("img"); + merchantLogoNode.className = "hyper-checkout-status-merchant-logo"; + // @ts-ignore + merchantLogoNode.src = window.__PAYMENT_DETAILS.merchant_logo; + merchantLogoNode.alt = ""; + + // Form content items + var statusImageNode = document.createElement("img"); + statusImageNode.className = "hyper-checkout-status-image"; + statusImageNode.src = statusDetails.imageSource; + var statusTextNode = document.createElement("div"); + statusTextNode.className = "hyper-checkout-status-text"; + statusTextNode.innerText = statusDetails.status; + var statusMessageNode = document.createElement("div"); + statusMessageNode.className = "hyper-checkout-status-message"; + statusMessageNode.innerText = statusDetails.message; + var statusDetailsNode = document.createElement("div"); + statusDetailsNode.className = "hyper-checkout-status-details"; + + // Append items + if (statusDetailsNode instanceof HTMLDivElement) { + statusDetails.items.map(function (item) { + statusDetailsNode.append(item); + }); + } + var statusHeaderNode = document.getElementById( + "hyper-checkout-status-header" + ); + if (statusHeaderNode instanceof HTMLDivElement) { + statusHeaderNode.append(amountNode, merchantLogoNode); + } + var statusContentNode = document.getElementById( + "hyper-checkout-status-content" + ); + if (statusContentNode instanceof HTMLDivElement) { + statusContentNode.append(statusImageNode, statusTextNode); + if (statusMessageNode instanceof HTMLDivElement) { + statusContentNode.append(statusMessageNode); + } + statusContentNode.append(statusDetailsNode); + } + + if (paymentDetails.redirect === true) { + // Form redirect text + var statusRedirectTextNode = document.getElementById( + "hyper-checkout-status-redirect-message" + ); + if ( + statusRedirectTextNode instanceof HTMLDivElement && + typeof paymentDetails.return_url === "string" + ) { + var timeout = 5, + j = 0; + for (var i = 0; i <= timeout; i++) { + setTimeout(function () { + var secondsLeft = timeout - j++; + var innerText = + secondsLeft === 0 + ? "Redirecting ..." + : "Redirecting in " + secondsLeft + " seconds ..."; + // @ts-ignore + statusRedirectTextNode.innerText = innerText; + if (secondsLeft === 0) { + // Form query params + var queryParams = { + payment_id: paymentDetails.payment_id, + status: paymentDetails.status, + }; + var url = new URL(paymentDetails.return_url); + var params = new URLSearchParams(url.search); + // Attach query params to return_url + for (var key in queryParams) { + if (queryParams.hasOwnProperty(key)) { + params.set(key, queryParams[key]); + } + } + url.search = params.toString(); + setTimeout(function () { + // Finally redirect + window.location.href = url.toString(); + }, 1000); + } + }, i * 1000); + } + } + } +} + +/** + * Use - create an item which is a key-value pair of some information related to a payment + * @param {String} heading + * @param {String} value + **/ +function createItem(heading, value) { + var itemNode = document.createElement("div"); + itemNode.className = "hyper-checkout-status-item"; + var headerNode = document.createElement("div"); + headerNode.className = "hyper-checkout-item-header"; + headerNode.innerText = heading; + var valueNode = document.createElement("div"); + valueNode.className = "hyper-checkout-item-value"; + valueNode.innerText = value; + itemNode.append(headerNode); + itemNode.append(valueNode); + return itemNode; +} + +/** + * Use - add event listeners for changing UI on screen resize + * @param {PaymentDetails} paymentDetails + */ +function initializeEventListeners(paymentDetails) { + var primaryColor = paymentDetails.theme; + var contrastBWColor = invert(primaryColor, true); + var statusRedirectTextNode = document.getElementById( + "hyper-checkout-status-redirect-message" + ); + + if (window.innerWidth <= 1400) { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = "#333333"; + } + } else if (window.innerWidth > 1400) { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = contrastBWColor; + } + } + + window.addEventListener("resize", function (event) { + var currentHeight = window.innerHeight; + var currentWidth = window.innerWidth; + // @ts-ignore + if (currentWidth <= 1400 && window.state.prevWidth > 1400) { + try { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = "#333333"; + } + } catch (error) { + console.error("Failed to fetch primary-color, using default", error); + } + // @ts-ignore + } else if (currentWidth > 1400 && window.state.prevWidth <= 1400) { + try { + if (statusRedirectTextNode instanceof HTMLDivElement) { + statusRedirectTextNode.style.color = contrastBWColor; + } + } catch (error) { + console.error("Failed to revert back to default colors", error); + } + } + + // @ts-ignore + window.state.prevHeight = currentHeight; + // @ts-ignore + window.state.prevWidth = currentWidth; + // @ts-ignore + window.state.isMobileView = currentWidth <= 1400; + }); +} diff --git a/crates/router/src/core/payment_link/status.html b/crates/router/src/core/payment_link/status.html deleted file mode 100644 index d3bb97d294dd..000000000000 --- a/crates/router/src/core/payment_link/status.html +++ /dev/null @@ -1,355 +0,0 @@ - - - - - 404 Not Found - - - - - - -
-
-
-
-
-
- - diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ae0328c56f6a..4a726084c2c8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -892,6 +892,10 @@ impl PaymentLink { web::resource("{merchant_id}/{payment_id}") .route(web::get().to(initiate_payment_link)), ) + .service( + web::resource("status/{merchant_id}/{payment_id}") + .route(web::get().to(payment_link_status)), + ) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 07894afe7323..4cd85efe8d50 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -147,9 +147,10 @@ impl From for ApiIdentifier { | Flow::BusinessProfileDelete | Flow::BusinessProfileList => Self::Business, - Flow::PaymentLinkRetrieve | Flow::PaymentLinkInitiate | Flow::PaymentLinkList => { - Self::PaymentLink - } + Flow::PaymentLinkRetrieve + | Flow::PaymentLinkInitiate + | Flow::PaymentLinkList + | Flow::PaymentLinkStatus => Self::PaymentLink, Flow::Verification => Self::Verification, diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index d45d67568b89..4d79b9231637 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -123,3 +123,33 @@ pub async fn payments_link_list( ) .await } + +pub async fn payment_link_status( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path<(String, String)>, +) -> impl Responder { + let flow = Flow::PaymentLinkStatus; + let (merchant_id, payment_id) = path.into_inner(); + let payload = api_models::payments::PaymentLinkInitiateRequest { + payment_id, + merchant_id: merchant_id.clone(), + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload.clone(), + |state, auth, _| { + get_payment_link_status( + state, + auth.merchant_account, + payload.merchant_id.clone(), + payload.payment_id.clone(), + ) + }, + &crate::services::authentication::MerchantIdAuth(merchant_id), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index e0c6a0862579..4251679d5d39 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1749,19 +1749,50 @@ pub fn build_redirection_form( pub fn build_payment_link_html( payment_link_data: PaymentLinkFormData, ) -> CustomResult { - let html_template = include_str!("../core/payment_link/payment_link.html").to_string(); - let mut tera = Tera::default(); + // Add modification to css template with dynamic data + let css_template = + include_str!("../core/payment_link/payment_link_initiate/payment_link.css").to_string(); + let _ = tera.add_raw_template("payment_link_css", &css_template); + let mut context = Context::new(); + context.insert("css_color_scheme", &payment_link_data.css_script); + + let rendered_css = match tera.render("payment_link_css", &context) { + Ok(rendered_css) => rendered_css, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Add modification to js template with dynamic data + let js_template = + include_str!("../core/payment_link/payment_link_initiate/payment_link.js").to_string(); + let _ = tera.add_raw_template("payment_link_js", &js_template); + + context.insert("payment_details_js_script", &payment_link_data.js_script); + + let rendered_js = match tera.render("payment_link_js", &context) { + Ok(rendered_js) => rendered_js, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Modify Html template with rendered js and rendered css files + let html_template = + include_str!("../core/payment_link/payment_link_initiate/payment_link.html").to_string(); + let _ = tera.add_raw_template("payment_link", &html_template); - let mut context = Context::new(); context.insert( "hyperloader_sdk_link", &get_hyper_loader_sdk(&payment_link_data.sdk_url), ); - context.insert("css_color_scheme", &payment_link_data.css_script); - context.insert("payment_details_js_script", &payment_link_data.js_script); + context.insert("rendered_css", &rendered_css); + context.insert("rendered_js", &rendered_js); match tera.render("payment_link", &context) { Ok(rendered_html) => Ok(rendered_html), @@ -1779,14 +1810,46 @@ fn get_hyper_loader_sdk(sdk_url: &str) -> String { pub fn get_payment_link_status( payment_link_data: PaymentLinkStatusData, ) -> CustomResult { - let html_template = include_str!("../core/payment_link/status.html").to_string(); let mut tera = Tera::default(); - let _ = tera.add_raw_template("payment_link_status", &html_template); + // Add modification to css template with dynamic data + let css_template = + include_str!("../core/payment_link/payment_link_status/status.css").to_string(); + let _ = tera.add_raw_template("payment_link_css", &css_template); let mut context = Context::new(); context.insert("css_color_scheme", &payment_link_data.css_script); + + let rendered_css = match tera.render("payment_link_css", &context) { + Ok(rendered_css) => rendered_css, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Add modification to js template with dynamic data + let js_template = + include_str!("../core/payment_link/payment_link_status/status.js").to_string(); + let _ = tera.add_raw_template("payment_link_js", &js_template); context.insert("payment_details_js_script", &payment_link_data.js_script); + let rendered_js = match tera.render("payment_link_js", &context) { + Ok(rendered_js) => rendered_js, + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + }; + + // Modify Html template with rendered js and rendered css files + let html_template = + include_str!("../core/payment_link/payment_link_status/status.html").to_string(); + let _ = tera.add_raw_template("payment_link_status", &html_template); + + context.insert("rendered_css", &rendered_css); + + context.insert("rendered_js", &rendered_js); + match tera.render("payment_link_status", &context) { Ok(rendered_html) => Ok(rendered_html), Err(tera_error) => { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0d5710820ee6..ac2dfb47c63d 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -241,6 +241,8 @@ pub enum Flow { PaymentLinkInitiate, /// Payment Link List flow PaymentLinkList, + /// Payment Link Status + PaymentLinkStatus, /// Create a business profile BusinessProfileCreate, /// Update a business profile From bec4f2a24e2236f7814119a6ebf0363cbf598540 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 30 Jan 2024 10:43:04 +0000 Subject: [PATCH 412/443] fix: empty payment attempts on payment retrieve (#3447) --- .../src/payments/payment_attempt.rs | 23 ++++++++++++++----- crates/storage_impl/src/redis/kv_store.rs | 11 ++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index b8d71cb32b7d..ad52a8aaeabc 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -932,12 +932,23 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); - - kv_wrapper(self, KvOperation::::Scan("pa_*"), key) - .await - .change_context(errors::StorageError::KVError)? - .try_into_scan() - .change_context(errors::StorageError::KVError) + Box::pin(try_redis_get_else_try_database_get( + async { + kv_wrapper(self, KvOperation::::Scan("pa_*"), key) + .await? + .try_into_scan() + }, + || async { + self.router_store + .find_attempts_by_merchant_id_payment_id( + merchant_id, + payment_id, + storage_scheme, + ) + .await + }, + )) + .await } } } diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 9339b11a9b9c..ad57ad403d4a 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -131,7 +131,16 @@ where } KvOperation::Scan(pattern) => { - let result: Vec = redis_conn.hscan_and_deserialize(key, pattern, None).await?; + let result: Vec = redis_conn + .hscan_and_deserialize(key, pattern, None) + .await + .and_then(|result| { + if result.is_empty() { + Err(RedisError::NotFound).into_report() + } else { + Ok(result) + } + })?; Ok(KvResult::Scan(result)) } From ac491038b16c77fc7f2249042b35dfb1d58e653d Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Tue, 30 Jan 2024 16:25:11 +0530 Subject: [PATCH 413/443] fix(logging): add flow to persistent logs fields (#3472) --- crates/router/src/middleware.rs | 5 +++-- crates/router_env/src/logger/storage.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/router/src/middleware.rs b/crates/router/src/middleware.rs index 587a15693f2e..702c3c70e6c6 100644 --- a/crates/router/src/middleware.rs +++ b/crates/router/src/middleware.rs @@ -58,7 +58,7 @@ where let request_id = request_id_fut.await?; let request_id = request_id.as_hyphenated().to_string(); if let Some(upstream_request_id) = old_x_request_id { - router_env::logger::info!(?request_id, ?upstream_request_id); + router_env::logger::info!(?upstream_request_id); } let mut response = response_fut.await?; response.headers_mut().append( @@ -146,7 +146,8 @@ where "golden_log_line", payment_id = Empty, merchant_id = Empty, - connector_name = Empty + connector_name = Empty, + flow = "UNKNOWN" ) .or_current(), ), diff --git a/crates/router_env/src/logger/storage.rs b/crates/router_env/src/logger/storage.rs index 961a77c65aa7..036a73cf874d 100644 --- a/crates/router_env/src/logger/storage.rs +++ b/crates/router_env/src/logger/storage.rs @@ -92,7 +92,7 @@ impl Visit for Storage<'_> { } } -const PERSISTENT_KEYS: [&str; 3] = ["payment_id", "connector_name", "merchant_id"]; +const PERSISTENT_KEYS: [&str; 4] = ["payment_id", "connector_name", "merchant_id", "flow"]; impl tracing_subscriber::registry::LookupSpan<'a>> Layer for StorageSubscription From 8c0c49c6bb02d4ec58242bc90eadfb267c24481e Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 30 Jan 2024 11:35:05 +0000 Subject: [PATCH 414/443] chore(kv): add metrics while pushing to stream (#3364) --- crates/storage_impl/src/lib.rs | 5 +++++ crates/storage_impl/src/metrics.rs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 7e2c7f2fc3c5..d31f05b4979b 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -225,6 +225,11 @@ impl KVRouterStore { .change_context(RedisError::JsonSerializationFailed)?, ) .await + .map(|_| metrics::KV_PUSHED_TO_DRAINER.add(&metrics::CONTEXT, 1, &[])) + .map_err(|err| { + metrics::KV_FAILED_TO_PUSH_TO_DRAINER.add(&metrics::CONTEXT, 1, &[]); + err + }) .change_context(RedisError::StreamAppendFailed) } } diff --git a/crates/storage_impl/src/metrics.rs b/crates/storage_impl/src/metrics.rs index 3310e458879b..29bca2a007b7 100644 --- a/crates/storage_impl/src/metrics.rs +++ b/crates/storage_impl/src/metrics.rs @@ -8,3 +8,5 @@ counter_metric!(KV_MISS, GLOBAL_METER); // No. of KV misses // Metrics for KV counter_metric!(KV_OPERATION_SUCCESSFUL, GLOBAL_METER); counter_metric!(KV_OPERATION_FAILED, GLOBAL_METER); +counter_metric!(KV_PUSHED_TO_DRAINER, GLOBAL_METER); +counter_metric!(KV_FAILED_TO_PUSH_TO_DRAINER, GLOBAL_METER); From 864a8d7b02acda5ea593cae83594962ea249c16d Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:06:31 +0530 Subject: [PATCH 415/443] feat(connector): [Stripe] Metadata to connector metadata mapping (#3295) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connector/stripe/transformers.rs | 83 ++++++++++++------- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/types.rs | 1 + 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 70582c41aa4d..8d397d0ebb9a 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1,4 +1,4 @@ -use std::ops::Deref; +use std::{collections::HashMap, ops::Deref}; use api_models::{self, enums as api_enums, payments}; use common_utils::{ @@ -9,11 +9,11 @@ use common_utils::{ }; use data_models::mandates::AcceptanceType; use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, ExposeOptionInterface, Secret}; +use masking::{ExposeInterface, ExposeOptionInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use time::PrimitiveDateTime; use url::Url; -use uuid::Uuid; use crate::{ collect_missing_value_keys, @@ -105,7 +105,7 @@ pub struct PaymentIntentRequest { pub statement_descriptor_suffix: Option, pub statement_descriptor: Option, #[serde(flatten)] - pub meta_data: StripeMetadata, + pub meta_data: HashMap, pub return_url: String, pub confirm: bool, pub mandate: Option>, @@ -145,12 +145,6 @@ pub struct StripeMetadata { #[derive(Debug, Eq, PartialEq, Serialize)] pub struct SetupIntentRequest { - #[serde(rename = "metadata[order_id]")] - pub metadata_order_id: String, - #[serde(rename = "metadata[txn_id]")] - pub metadata_txn_id: String, - #[serde(rename = "metadata[txn_uuid]")] - pub metadata_txn_uuid: String, pub confirm: bool, pub usage: Option, pub customer: Option>, @@ -159,6 +153,8 @@ pub struct SetupIntentRequest { #[serde(flatten)] pub payment_data: StripePaymentMethodData, pub payment_method_options: Option, // For mandate txns using network_txns_id, needs to be validated + #[serde(flatten)] + pub meta_data: Option>, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -218,6 +214,8 @@ pub struct ChargesRequest { pub currency: String, pub customer: Secret, pub source: Secret, + #[serde(flatten)] + pub meta_data: Option>, } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] @@ -1877,15 +1875,15 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest { None } }); + + let meta_data = get_transaction_metadata(item.request.metadata.clone(), order_id); + Ok(Self { amount: item.request.amount, //hopefully we don't loose some cents here currency: item.request.currency.to_string(), //we need to copy the value and not transfer ownership statement_descriptor_suffix: item.request.statement_descriptor_suffix.clone(), statement_descriptor: item.request.statement_descriptor.clone(), - meta_data: StripeMetadata { - order_id: Some(order_id), - is_refund_id_as_reference: None, - }, + meta_data, return_url: item .request .router_return_url @@ -1945,10 +1943,6 @@ fn get_payment_method_type_for_saved_payment_method_payment( impl TryFrom<&types::SetupMandateRouterData> for SetupIntentRequest { type Error = error_stack::Report; fn try_from(item: &types::SetupMandateRouterData) -> Result { - let metadata_order_id = item.connector_request_reference_id.clone(); - let metadata_txn_id = format!("{}_{}_{}", item.merchant_id, item.payment_id, "1"); - let metadata_txn_uuid = Uuid::new_v4().to_string(); - //Only cards supported for mandates let pm_type = StripePaymentMethodType::Card; let payment_data = StripePaymentMethodData::try_from(( @@ -1957,17 +1951,20 @@ impl TryFrom<&types::SetupMandateRouterData> for SetupIntentRequest { pm_type, ))?; + let meta_data = Some(get_transaction_metadata( + item.request.metadata.clone(), + item.connector_request_reference_id.clone(), + )); + Ok(Self { confirm: true, - metadata_order_id, - metadata_txn_id, - metadata_txn_uuid, payment_data, return_url: item.request.router_return_url.clone(), off_session: item.request.off_session, usage: item.request.setup_future_usage, payment_method_options: None, customer: item.connector_customer.to_owned().map(Secret::new), + meta_data, }) } } @@ -2355,7 +2352,7 @@ impl pub fn get_connector_metadata( next_action: Option<&StripeNextActionResponse>, amount: i64, -) -> CustomResult, errors::ConnectorError> { +) -> CustomResult, errors::ConnectorError> { let next_action_response = next_action .and_then(|next_action_response| match next_action_response { StripeNextActionResponse::DisplayBankTransferInstructions(response) => { @@ -3058,12 +3055,20 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for ChargesRequest { type Error = error_stack::Report; fn try_from(value: &types::PaymentsAuthorizeRouterData) -> Result { - Ok(Self { - amount: value.request.amount.to_string(), - currency: value.request.currency.to_string(), - customer: Secret::new(value.get_connector_customer_id()?), - source: Secret::new(value.get_preprocessing_id()?), - }) + { + let order_id = value.connector_request_reference_id.clone(); + let meta_data = Some(get_transaction_metadata( + value.request.metadata.clone(), + order_id, + )); + Ok(Self { + amount: value.request.amount.to_string(), + currency: value.request.currency.to_string(), + customer: Secret::new(value.get_connector_customer_id()?), + source: Secret::new(value.get_preprocessing_id()?), + meta_data, + }) + } } } @@ -3169,7 +3174,7 @@ impl #[derive(Debug, Deserialize)] pub struct WebhookEventDataResource { - pub object: serde_json::Value, + pub object: Value, } #[derive(Debug, Deserialize)] @@ -3649,6 +3654,26 @@ pub struct DisputeObj { pub status: String, } +fn get_transaction_metadata( + merchant_metadata: Option>, + order_id: String, +) -> HashMap { + let mut meta_data = HashMap::from([("metadata[order_id]".to_string(), order_id)]); + let mut request_hash_map = HashMap::new(); + + if let Some(metadata) = merchant_metadata { + let hashmap: HashMap = + serde_json::from_str(&metadata.peek().to_string()).unwrap_or(HashMap::new()); + + for (key, value) in hashmap { + request_hash_map.insert(format!("metadata[{}]", key), value.to_string()); + } + + meta_data.extend(request_hash_map) + }; + meta_data +} + #[cfg(test)] mod test_validate_shipping_address_against_payment_method { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 61917fdcd2e6..5b05e6015023 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1440,6 +1440,7 @@ impl TryFrom> for types::SetupMandateRequ Some(RequestIncrementalAuthorization::True) | Some(RequestIncrementalAuthorization::Default) ), + metadata: payment_data.payment_intent.metadata.clone(), }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 0809ca178203..531ca849a352 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -591,6 +591,7 @@ pub struct SetupMandateRequestData { pub return_url: Option, pub payment_method_type: Option, pub request_incremental_authorization: bool, + pub metadata: Option, } #[derive(Debug, Clone)] From 431ccb156b27fe104063e997a8b5d97021e5f960 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:55:37 +0000 Subject: [PATCH 416/443] chore(version): 2024.01.30.1 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2053240e80d..dd32d16d5964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.30.1 + +### Features + +- **config:** Add iDEAL and Sofort Env Configs ([#3492](https://github.com/juspay/hyperswitch/pull/3492)) ([`46c1822`](https://github.com/juspay/hyperswitch/commit/46c1822d0e367e59420c9d087428bc3b12794445)) +- **connector:** + - [Bluesnap] Metadata to connector metadata mapping ([#3331](https://github.com/juspay/hyperswitch/pull/3331)) ([`b2afdc3`](https://github.com/juspay/hyperswitch/commit/b2afdc35465426bd11428d8d4ac743617a443128)) + - [Stripe] Metadata to connector metadata mapping ([#3295](https://github.com/juspay/hyperswitch/pull/3295)) ([`864a8d7`](https://github.com/juspay/hyperswitch/commit/864a8d7b02acda5ea593cae83594962ea249c16d)) +- **core:** Update card_details for an existing mandate ([#3452](https://github.com/juspay/hyperswitch/pull/3452)) ([`02074df`](https://github.com/juspay/hyperswitch/commit/02074dfc23f1a126e76935ba5311c6aed6590ca5)) +- **pm_list:** Add required fields for sofort ([#3192](https://github.com/juspay/hyperswitch/pull/3192)) ([`3d55e3b`](https://github.com/juspay/hyperswitch/commit/3d55e3ba45619978e8ca9e5012c156dc017d2879)) +- **users:** Signin and Verify Email changes for User Invitation changes ([#3420](https://github.com/juspay/hyperswitch/pull/3420)) ([`d91da89`](https://github.com/juspay/hyperswitch/commit/d91da89065a6870f05e1ff9db007d16a58454c84)) + +### Bug Fixes + +- **logging:** Add flow to persistent logs fields ([#3472](https://github.com/juspay/hyperswitch/pull/3472)) ([`ac49103`](https://github.com/juspay/hyperswitch/commit/ac491038b16c77fc7f2249042b35dfb1d58e653d)) +- Empty payment attempts on payment retrieve ([#3447](https://github.com/juspay/hyperswitch/pull/3447)) ([`bec4f2a`](https://github.com/juspay/hyperswitch/commit/bec4f2a24e2236f7814119a6ebf0363cbf598540)) + +### Refactors + +- **payment_link:** Segregated payment link in html css js files, sdk over flow issue, surcharge bug, block SPM customer call for payment link ([#3410](https://github.com/juspay/hyperswitch/pull/3410)) ([`a7bc8c6`](https://github.com/juspay/hyperswitch/commit/a7bc8c655f5b745dccd4d818ac3ceb08c3b80c0e)) +- **settings:** Make the function to deserialize hashsets more generic ([#3104](https://github.com/juspay/hyperswitch/pull/3104)) ([`87191d6`](https://github.com/juspay/hyperswitch/commit/87191d687cd66bf096bfb98ffe51a805b4b76a03)) +- Add support for extending file storage to other schemes and provide a runtime flag for the same ([#3348](https://github.com/juspay/hyperswitch/pull/3348)) ([`a9638d1`](https://github.com/juspay/hyperswitch/commit/a9638d118e0b68653fef3bec2ce8aa3c47feedd3)) + +### Miscellaneous Tasks + +- **analytics:** + - Adding status code to connector Kafka events ([#3393](https://github.com/juspay/hyperswitch/pull/3393)) ([`d6807ab`](https://github.com/juspay/hyperswitch/commit/d6807abba46136eabadcbfbc51bce421144dca2c)) + - Adding dispute id to api log events ([#3450](https://github.com/juspay/hyperswitch/pull/3450)) ([`937aea9`](https://github.com/juspay/hyperswitch/commit/937aea906e759e6e8a76a424db99ed052d46b7d2)) +- **kv:** Add metrics while pushing to stream ([#3364](https://github.com/juspay/hyperswitch/pull/3364)) ([`8c0c49c`](https://github.com/juspay/hyperswitch/commit/8c0c49c6bb02d4ec58242bc90eadfb267c24481e)) + +**Full Changelog:** [`2024.01.30.0...2024.01.30.1`](https://github.com/juspay/hyperswitch/compare/2024.01.30.0...2024.01.30.1) + +- - - + ## 2024.01.30.0 ### Features From b5bc8c4e7cfdde8251ed0e2e3835ed5e3f1435c4 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:15:04 +0530 Subject: [PATCH 417/443] feat(connector): [noon] add revoke mandate (#3487) --- crates/router/src/connector/noon.rs | 76 +++++++++++++++ .../router/src/connector/noon/transformers.rs | 95 ++++++++++++++++++- crates/router/src/core/payments/flows.rs | 1 - 3 files changed, 167 insertions(+), 5 deletions(-) diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 180b4b1485fb..6c98a3076375 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -46,6 +46,7 @@ impl api::Refund for Noon {} impl api::RefundExecute for Noon {} impl api::RefundSync for Noon {} impl api::PaymentToken for Noon {} +impl api::ConnectorMandateRevoke for Noon {} impl ConnectorIntegration< @@ -492,6 +493,81 @@ impl ConnectorIntegration for Noon +{ + fn get_headers( + &self, + req: &types::MandateRevokeRouterData, + 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::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}payment/v1/order", self.base_url(connectors))) + } + fn build_request( + &self, + req: &types::MandateRevokeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::MandateRevokeType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::MandateRevokeType::get_headers( + self, req, connectors, + )?) + .set_body(types::MandateRevokeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn get_request_body( + &self, + req: &types::MandateRevokeRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_req = noon::NoonRevokeMandateRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn handle_response( + &self, + data: &types::MandateRevokeRouterData, + res: Response, + ) -> CustomResult { + let response: noon::NoonRevokeMandateResponse = res + .response + .parse_struct("Noon NoonRevokeMandateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration for Noon { fn get_headers( &self, diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index bbf284848b59..ee06cd064bed 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -5,7 +5,8 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, + self as conn_utils, CardData, PaymentsAuthorizeRequestData, RevokeMandateRequestData, + RouterData, WalletData, }, core::errors, services, @@ -30,11 +31,13 @@ pub enum NoonSubscriptionType { } #[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct NoonSubscriptionData { #[serde(rename = "type")] subscription_type: NoonSubscriptionType, //Short description about the subscription. name: String, + max_amount: Option, } #[derive(Debug, Serialize)] @@ -168,12 +171,13 @@ pub enum NoonPaymentData { } #[derive(Debug, Serialize)] -#[serde(rename_all = "UPPERCASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum NoonApiOperations { Initiate, Capture, Reverse, Refund, + CancelSubscription, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -335,6 +339,21 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { NoonSubscriptionData { subscription_type: NoonSubscriptionType::Unscheduled, name: name.clone(), + max_amount: item + .request + .setup_mandate_details + .clone() + .and_then(|mandate_details| match mandate_details.mandate_type { + Some(data_models::mandates::MandateDataType::SingleUse(mandate)) + | Some(data_models::mandates::MandateDataType::MultiUse(Some( + mandate, + ))) => Some( + conn_utils::to_currency_base_unit(mandate.amount, mandate.currency) + .ok(), + ), + _ => None, + }) + .flatten(), }, true, )) { @@ -450,7 +469,7 @@ impl ForeignFrom<(NoonPaymentStatus, Self)> for enums::AttemptStatus { } #[derive(Debug, Serialize, Deserialize)] -pub struct NoonSubscriptionResponse { +pub struct NoonSubscriptionObject { identifier: String, } @@ -475,7 +494,7 @@ pub struct NoonCheckoutData { pub struct NoonPaymentsResponseResult { order: NoonPaymentsOrderResponse, checkout_data: Option, - subscription: Option, + subscription: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -603,6 +622,25 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NoonPaymentsCancelRequest { } } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NoonRevokeMandateRequest { + api_operation: NoonApiOperations, + subscription: NoonSubscriptionObject, +} + +impl TryFrom<&types::MandateRevokeRouterData> for NoonRevokeMandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::MandateRevokeRouterData) -> Result { + Ok(Self { + api_operation: NoonApiOperations::CancelSubscription, + subscription: NoonSubscriptionObject { + identifier: item.request.get_connector_mandate_id()?, + }, + }) + } +} + impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { @@ -624,6 +662,55 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { }) } } +#[derive(Debug, Deserialize)] +pub enum NoonRevokeStatus { + Cancelled, +} + +#[derive(Debug, Deserialize)] +pub struct NoonCancelSubscriptionObject { + status: NoonRevokeStatus, +} + +#[derive(Debug, Deserialize)] +pub struct NoonRevokeMandateResult { + subscription: NoonCancelSubscriptionObject, +} + +#[derive(Debug, Deserialize)] +pub struct NoonRevokeMandateResponse { + result: NoonRevokeMandateResult, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + NoonRevokeMandateResponse, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + NoonRevokeMandateResponse, + types::MandateRevokeRequestData, + types::MandateRevokeResponseData, + >, + ) -> Result { + match item.response.result.subscription.status { + NoonRevokeStatus::Cancelled => Ok(Self { + response: Ok(types::MandateRevokeResponseData { + mandate_status: common_enums::MandateStatus::Revoked, + }), + ..item.data + }), + } + } +} #[derive(Debug, Default, Deserialize, Clone)] #[serde(rename_all = "UPPERCASE")] diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c9f9d6d87f5c..ebc0cf3664af 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -2247,7 +2247,6 @@ default_imp_for_revoking_mandates!( connector::Multisafepay, connector::Nexinets, connector::Nmi, - connector::Noon, connector::Nuvei, connector::Opayo, connector::Opennode, From d2accdef410319733d6174057bdca468bde1ae83 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:16:02 +0530 Subject: [PATCH 418/443] refactor(core): restrict requires_customer_action in confirm (#3235) --- crates/api_models/src/payments.rs | 9 + crates/common_enums/src/enums.rs | 1 + crates/router/src/core/payments.rs | 1 + crates/router/src/core/payments/operations.rs | 1 + .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 38 +- .../payments/operations/payment_create.rs | 1 + .../operations/payment_method_validate.rs | 398 ------------------ .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../payments_incremental_authorization.rs | 1 + crates/router/src/core/webhooks.rs | 2 +- 18 files changed, 51 insertions(+), 410 deletions(-) delete mode 100644 crates/router/src/core/payments/operations/payment_method_validate.rs diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index c856ae327955..3b85644553d6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -403,6 +403,15 @@ pub struct HeaderPayload { pub x_hs_latency: Option, } +impl HeaderPayload { + pub fn with_source(payment_confirm_source: api_enums::PaymentSource) -> Self { + Self { + payment_confirm_source: Some(payment_confirm_source), + ..Default::default() + } + } +} + #[derive( Default, Debug, serde::Serialize, Clone, PartialEq, ToSchema, router_derive::PolymorphicSchema, )] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 7eb65f18a329..7330c0708d58 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2069,6 +2069,7 @@ pub enum PaymentSource { Postman, Dashboard, Sdk, + Webhook, } #[derive( diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 186f760ace18..fd265c07da28 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -129,6 +129,7 @@ where &merchant_account, &key_store, auth_flow, + header_payload.payment_confirm_source, ) .await?; diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 716b76120d8a..89d3131ddef1 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -110,6 +110,7 @@ pub trait GetTracker: Send { merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, + payment_confirm_source: Option, ) -> RouterResult>; } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 6d3697caabdf..ce3998506c9e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -42,6 +42,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 9810980cd34a..fecf3971019a 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -42,6 +42,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 3986b16ce353..acf2ce431195 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -41,6 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index adc137403e5d..b4f538e1d089 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -44,6 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 14fc28d67237..151078a6056f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -56,6 +56,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, + payment_confirm_source: Option, ) -> RouterResult> { let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -117,17 +118,32 @@ impl helpers::validate_customer_access(&payment_intent, auth_flow, request)?; - helpers::validate_payment_status_against_not_allowed_statuses( - &payment_intent.status, - &[ - storage_enums::IntentStatus::Cancelled, - storage_enums::IntentStatus::Succeeded, - storage_enums::IntentStatus::Processing, - storage_enums::IntentStatus::RequiresCapture, - storage_enums::IntentStatus::RequiresMerchantAction, - ], - "confirm", - )?; + if let Some(common_enums::PaymentSource::Webhook) = payment_confirm_source { + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + ], + "confirm", + )?; + } else { + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + storage_enums::IntentStatus::RequiresCustomerAction, + ], + "confirm", + )?; + } helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?; diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index d02ad15fbd64..381f02659769 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -59,6 +59,7 @@ impl merchant_account: &domain::MerchantAccount, merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let db = &*state.store; let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await; diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs deleted file mode 100644 index 9ea347afd735..000000000000 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ /dev/null @@ -1,398 +0,0 @@ -use std::marker::PhantomData; - -use api_models::enums::FrmSuggestion; -use async_trait::async_trait; -use common_utils::{date_time, errors::CustomResult, ext_traits::AsyncExt}; -use error_stack::ResultExt; -use router_derive::PaymentOperation; -use router_env::{instrument, tracing}; - -use super::{BoxedOperation, Domain, GetTracker, UpdateTracker, ValidateRequest}; -use crate::{ - consts, - core::{ - errors::{self, RouterResult, StorageErrorExt}, - payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, Operation, PaymentData}, - utils as core_utils, - }, - db::StorageInterface, - routes::AppState, - services, - types::{ - self, - api::{self, enums as api_enums, PaymentIdTypeExt}, - domain, - storage::{self, enums as storage_enums}, - }, - utils, -}; - -#[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(operations = "all", flow = "verify")] -pub struct PaymentMethodValidate; - -impl ValidateRequest - for PaymentMethodValidate -{ - #[instrument(skip_all)] - fn validate_request<'a, 'b>( - &'b self, - request: &api::VerifyRequest, - merchant_account: &'a domain::MerchantAccount, - ) -> RouterResult<( - BoxedOperation<'b, F, api::VerifyRequest, Ctx>, - operations::ValidateResult<'a>, - )> { - let request_merchant_id = request.merchant_id.as_deref(); - helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) - .change_context(errors::ApiErrorResponse::MerchantAccountNotFound)?; - - let mandate_type = - helpers::validate_mandate(request, payments::is_operation_confirm(self))?; - let validation_id = core_utils::get_or_generate_id("validation_id", &None, "val")?; - - Ok(( - Box::new(self), - operations::ValidateResult { - merchant_id: &merchant_account.merchant_id, - payment_id: api::PaymentIdType::PaymentIntentId(validation_id), - mandate_type, - storage_scheme: merchant_account.storage_scheme, - requeue: false, - }, - )) - } -} - -#[async_trait] -impl - GetTracker, api::VerifyRequest, Ctx> for PaymentMethodValidate -{ - #[instrument(skip_all)] - async fn get_trackers<'a>( - &'a self, - state: &'a AppState, - payment_id: &api::PaymentIdType, - request: &api::VerifyRequest, - _mandate_type: Option, - merchant_account: &domain::MerchantAccount, - _mechant_key_store: &domain::MerchantKeyStore, - _auth_flow: services::AuthFlow, - ) -> RouterResult<( - BoxedOperation<'a, F, api::VerifyRequest, Ctx>, - PaymentData, - Option, - )> { - let db = &*state.store; - - let merchant_id = &merchant_account.merchant_id; - let storage_scheme = merchant_account.storage_scheme; - - let (payment_intent, payment_attempt); - - let payment_id = payment_id - .get_payment_intent_id() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting payment_intent_id from PaymentIdType")?; - - payment_attempt = match db - .insert_payment_attempt( - Self::make_payment_attempt( - &payment_id, - merchant_id, - request.payment_method, - request, - state, - merchant_account.storage_scheme, - ), - storage_scheme, - ) - .await - { - Ok(payment_attempt) => Ok(payment_attempt), - Err(err) => { - Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None })) - } - }?; - - payment_intent = match db - .insert_payment_intent( - Self::make_payment_intent( - &payment_id, - merchant_id, - request, - payment_attempt.attempt_id.clone(), - merchant_account.storage_scheme, - ), - storage_scheme, - ) - .await - { - Ok(payment_intent) => Ok(payment_intent), - Err(err) => { - Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None })) - } - }?; - - let creds_identifier = request - .merchant_connector_details - .as_ref() - .map(|mcd| mcd.creds_identifier.to_owned()); - request - .merchant_connector_details - .to_owned() - .async_map(|mcd| async { - helpers::insert_merchant_connector_creds_to_config( - db, - merchant_account.merchant_id.as_str(), - mcd, - ) - .await - }) - .await - .transpose()?; - - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - /// currency and amount are irrelevant in this scenario - currency: storage_enums::Currency::default(), - amount: api::Amount::Zero, - email: None, - mandate_id: None, - mandate_connector: None, - setup_mandate: request.mandate_data.clone().map(Into::into), - token: request.payment_token.clone(), - payment_method_data: request.payment_method_data.clone(), - confirm: Some(true), - address: types::PaymentAddress::default(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: None, - creds_identifier, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data: None, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response: None, - surcharge_details: None, - frm_message: None, - payment_link_data: None, - frm_metadata: None, - }, - Some(payments::CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) - } -} - -#[async_trait] -impl UpdateTracker, api::VerifyRequest, Ctx> - for PaymentMethodValidate -{ - #[instrument(skip_all)] - async fn update_trackers<'b>( - &'b self, - state: &'b AppState, - mut payment_data: PaymentData, - _customer: Option, - storage_scheme: storage_enums::MerchantStorageScheme, - _updated_customer: Option, - _mechant_key_store: &domain::MerchantKeyStore, - _frm_suggestion: Option, - _header_payload: api::HeaderPayload, - ) -> RouterResult<( - BoxedOperation<'b, F, api::VerifyRequest, Ctx>, - PaymentData, - )> - where - F: 'b + Send, - { - // There is no fsm involved in this operation all the change of states must happen in a single request - let status = Some(storage_enums::IntentStatus::Processing); - - let customer_id = payment_data.payment_intent.customer_id.clone(); - - payment_data.payment_intent = state - .store - .update_payment_intent( - payment_data.payment_intent, - storage::PaymentIntentUpdate::ReturnUrlUpdate { - return_url: None, - status, - customer_id, - shipping_address_id: None, - billing_address_id: None, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::VerificationFailed { data: None })?; - - Ok((Box::new(self), payment_data)) - } -} - -#[async_trait] -impl Domain for Op -where - F: Clone + Send, - Op: Send + Sync + Operation, - for<'a> &'a Op: Operation, -{ - #[instrument(skip_all)] - async fn get_or_create_customer_details<'a>( - &'a self, - db: &dyn StorageInterface, - payment_data: &mut PaymentData, - request: Option, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult< - ( - BoxedOperation<'a, F, api::VerifyRequest, Ctx>, - Option, - ), - errors::StorageError, - > { - helpers::create_customer_if_not_exist( - Box::new(self), - db, - payment_data, - request, - &key_store.merchant_id, - key_store, - ) - .await - } - - #[instrument(skip_all)] - async fn make_pm_data<'a>( - &'a self, - state: &'a AppState, - payment_data: &mut PaymentData, - _storage_scheme: storage_enums::MerchantStorageScheme, - merchant_key_store: &domain::MerchantKeyStore, - ) -> RouterResult<( - BoxedOperation<'a, F, api::VerifyRequest, Ctx>, - Option, - )> { - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await - } - - async fn get_connector<'a>( - &'a self, - _merchant_account: &domain::MerchantAccount, - state: &AppState, - _request: &api::VerifyRequest, - _payment_intent: &storage::PaymentIntent, - _mechant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - helpers::get_connector_default(state, None).await - } -} - -impl PaymentMethodValidate { - #[instrument(skip_all)] - fn make_payment_attempt( - payment_id: &str, - merchant_id: &str, - payment_method: Option, - _request: &api::VerifyRequest, - state: &AppState, - storage_scheme: storage_enums::MerchantStorageScheme, - ) -> storage::PaymentAttemptNew { - let created_at @ modified_at @ last_synced = Some(date_time::now()); - let status = storage_enums::AttemptStatus::Pending; - let attempt_id = if core_utils::is_merchant_enabled_for_payment_id_as_connector_request_id( - &state.conf, - merchant_id, - ) { - payment_id.to_string() - } else { - utils::get_payment_attempt_id(payment_id, 1) - }; - - storage::PaymentAttemptNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - attempt_id, - status, - // Amount & Currency will be zero in this case - amount: 0, - currency: Default::default(), - connector: None, - payment_method, - confirm: true, - created_at, - modified_at, - last_synced, - updated_by: storage_scheme.to_string(), - ..Default::default() - } - } - - fn make_payment_intent( - payment_id: &str, - merchant_id: &str, - request: &api::VerifyRequest, - active_attempt_id: String, - storage_scheme: storage_enums::MerchantStorageScheme, - ) -> storage::PaymentIntentNew { - let created_at @ modified_at @ last_synced = Some(date_time::now()); - let status = helpers::payment_intent_status_fsm(&request.payment_method_data, Some(true)); - - let client_secret = - utils::generate_id(consts::ID_LENGTH, format!("{payment_id}_secret").as_str()); - storage::PaymentIntentNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - status, - amount: 0, - currency: Default::default(), - connector_id: None, - created_at, - modified_at, - last_synced, - client_secret: Some(client_secret), - setup_future_usage: request.setup_future_usage, - off_session: request.off_session, - active_attempt: data_models::RemoteStorageObject::ForeignID(active_attempt_id), - attempt_count: 1, - amount_captured: Default::default(), - customer_id: Default::default(), - description: Default::default(), - return_url: Default::default(), - metadata: Default::default(), - shipping_address_id: Default::default(), - billing_address_id: Default::default(), - statement_descriptor_name: Default::default(), - statement_descriptor_suffix: Default::default(), - business_country: Default::default(), - business_label: Default::default(), - order_details: Default::default(), - allowed_payment_method_types: Default::default(), - connector_metadata: Default::default(), - feature_metadata: Default::default(), - profile_id: Default::default(), - merchant_decision: Default::default(), - payment_confirm_source: Default::default(), - surcharge_applicable: Default::default(), - payment_link_id: Default::default(), - updated_by: storage_scheme.to_string(), - } - } -} diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index e958422d3127..02d2e953c19c 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -41,6 +41,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 9a58dd5af76c..170db39388b7 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -43,6 +43,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let payment_id = payment_id .get_payment_intent_id() diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 557c5c0bd8c9..8b4ec4e3f546 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -42,6 +42,7 @@ impl merchant_account: &domain::MerchantAccount, mechant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let (mut payment_intent, payment_attempt, currency, amount); let db = &*state.store; diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index c6d9a30f0c95..9db9742ca9ea 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -198,6 +198,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { get_tracker_for_sync( diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 015ef5cea6ef..664fce820a0e 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -44,6 +44,7 @@ impl merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 51fdff77c3be..e7bb5622b749 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -46,6 +46,7 @@ impl merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, + _payment_confirm_source: Option, ) -> RouterResult< operations::GetTrackerResponse<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, > { diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index f291d1cd2e80..f0348e45eb30 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -603,7 +603,7 @@ async fn bank_transfer_webhook_flow Date: Tue, 30 Jan 2024 18:55:07 +0530 Subject: [PATCH 419/443] chore(config): [ADYEN] Add configs for PIX in WASM (#3498) --- config/deployments/sandbox.toml | 1 + config/development.toml | 1 + crates/connector_configs/toml/development.toml | 2 ++ crates/connector_configs/toml/sandbox.toml | 2 ++ 4 files changed, 6 insertions(+) diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index c58eff29edb6..14f49e01caf9 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -216,6 +216,7 @@ twint = { country = "CH", currency = "CHF" } vipps = { country = "NO", currency = "NOK" } walley = { country = "SE,NO,DK,FI", currency = "DKK,EUR,NOK,SEK" } we_chat_pay = { country = "AU,NZ,CN,JP,HK,SG,ES,GB,SE,NO,AT,NL,DE,CY,CH,BE,FR,DK,LI,MT,SI,GR,PT,IT,CA,US", currency = "AUD,CAD,CNY,EUR,GBP,HKD,JPY,NZD,SGD,USD" } +pix = { country = "BR", currency = "BRL" } [pm_filters.authorizedotnet] google_pay.currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" diff --git a/config/development.toml b/config/development.toml index d447930b5902..681db4ac5d18 100644 --- a/config/development.toml +++ b/config/development.toml @@ -348,6 +348,7 @@ mini_stop = {country = "JP", currency = "JPY"} family_mart = {country = "JP", currency = "JPY"} seicomart = {country = "JP", currency = "JPY"} pay_easy = {country = "JP", currency = "JPY"} +pix = { country = "BR", currency = "BRL" } [pm_filters.braintree] paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" } diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 07003e89e61e..a8188323dce8 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -163,6 +163,8 @@ merchant_secret="Source verification key" payment_method_type = "danamon_va" [[adyen.bank_transfer]] payment_method_type = "mandiri_va" +[[adyen.bank_transfer]] + payment_method_type = "pix" [[adyen.wallet]] payment_method_type = "apple_pay" [[adyen.wallet]] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 09883ead06ff..576cd47c6eec 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -163,6 +163,8 @@ merchant_secret="Source verification key" payment_method_type = "danamon_va" [[adyen.bank_transfer]] payment_method_type = "mandiri_va" +[[adyen.bank_transfer]] + payment_method_type = "pix" [[adyen.wallet]] payment_method_type = "apple_pay" [[adyen.wallet]] From 224c1cf2a421441433097618cc1dd3db224d5915 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:58:33 +0530 Subject: [PATCH 420/443] fix(connector): [BOA/Cybersource] Handle Invalid Api Secret (#3485) --- crates/router/src/connector/bankofamerica.rs | 4 +++- crates/router/src/connector/bankofamerica/transformers.rs | 2 +- crates/router/src/connector/cybersource.rs | 4 +++- crates/router/src/connector/cybersource/transformers.rs | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/bankofamerica.rs b/crates/router/src/connector/bankofamerica.rs index 0d901b990784..589852c6b56f 100644 --- a/crates/router/src/connector/bankofamerica.rs +++ b/crates/router/src/connector/bankofamerica.rs @@ -87,7 +87,9 @@ impl Bankofamerica { let key_value = consts::BASE64_ENGINE .decode(api_secret.expose()) .into_report() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "connector_account_details.api_secret", + })?; let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value); let signature_value = consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_string.as_bytes()).as_ref()); diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index db96ff62f6ca..0b8158c10af9 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -489,7 +489,7 @@ impl })? .parse_value("BankOfAmericaThreeDSMetadata") .change_context(errors::ConnectorError::InvalidConnectorConfig { - config: "Merchant connector account metadata", + config: "metadata", })?; let processing_information = diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index ac2d16c9610e..7970656d6e3d 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -79,7 +79,9 @@ impl Cybersource { let key_value = consts::BASE64_ENGINE .decode(api_secret.expose()) .into_report() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "connector_account_details.api_secret", + })?; let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value); let signature_value = consts::BASE64_ENGINE.encode(hmac::sign(&key, signature_string.as_bytes()).as_ref()); diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 659f0733fdce..9b0bf61c5458 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -770,7 +770,7 @@ impl })? .parse_value("CybersourceThreeDSMetadata") .change_context(errors::ConnectorError::InvalidConnectorConfig { - config: "Merchant connector account metadata", + config: "metadata", })?; let processing_information = From 610c1c575253ddf7a1a31ef941efaae2dd676b48 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:15:47 +0530 Subject: [PATCH 421/443] fix(user): change permission for sample data (#3462) --- crates/router/src/routes/lock_utils.rs | 4 ++-- crates/router/src/routes/user.rs | 4 ++-- crates/router/src/routes/user_role.rs | 2 +- crates/router_env/src/logger/types.rs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 4cd85efe8d50..2837c1defa4a 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -179,7 +179,6 @@ impl From for ApiIdentifier { | Flow::ResetPassword | Flow::InviteUser | Flow::InviteMultipleUser - | Flow::DeleteUser | Flow::UserSignUpWithMerchantId | Flow::VerifyEmailWithoutInviteChecks | Flow::VerifyEmail @@ -191,7 +190,8 @@ impl From for ApiIdentifier { | Flow::GetRoleFromToken | Flow::UpdateUserRole | Flow::GetAuthorizationInfo - | Flow::AcceptInvitation => Self::UserRole, + | Flow::AcceptInvitation + | Flow::DeleteUserRole => Self::UserRole, Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { Self::ConnectorOnboarding diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 88e19ddf7550..d4bdcaae87fc 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -257,7 +257,7 @@ pub async fn generate_sample_data( &http_req, payload.into_inner(), sample_data::generate_sample_data_for_user, - &auth::JWTAuth(Permission::MerchantAccountWrite), + &auth::JWTAuth(Permission::PaymentWrite), api_locking::LockAction::NotApplicable, )) .await @@ -277,7 +277,7 @@ pub async fn delete_sample_data( &http_req, payload.into_inner(), sample_data::delete_sample_data_for_user, - &auth::JWTAuth(Permission::MerchantAccountWrite), + &auth::JWTAuth(Permission::PaymentWrite), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 3f9ccda8651f..ec05db1d6150 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -121,7 +121,7 @@ pub async fn delete_user_role( req: HttpRequest, payload: web::Json, ) -> HttpResponse { - let flow = Flow::DeleteUser; + let flow = Flow::DeleteUserRole; Box::pin(api::server_wrap( flow, state.clone(), diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index ac2dfb47c63d..8e32cb633341 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -327,8 +327,8 @@ pub enum Flow { InviteUser, /// Invite multiple users InviteMultipleUser, - /// Delete user - DeleteUser, + /// Delete user role + DeleteUserRole, /// Incremental Authorization flow PaymentsIncrementalAuthorization, /// Get action URL for connector onboarding From 248d3c79f00a257dab759d15776de1887445cf07 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:19:11 +0000 Subject: [PATCH 422/443] chore(version): 2024.01.31.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd32d16d5964..00cfdc5a015a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.31.0 + +### Features + +- **connector:** [noon] add revoke mandate ([#3487](https://github.com/juspay/hyperswitch/pull/3487)) ([`b5bc8c4`](https://github.com/juspay/hyperswitch/commit/b5bc8c4e7cfdde8251ed0e2e3835ed5e3f1435c4)) + +### Bug Fixes + +- **connector:** [BOA/Cybersource] Handle Invalid Api Secret ([#3485](https://github.com/juspay/hyperswitch/pull/3485)) ([`224c1cf`](https://github.com/juspay/hyperswitch/commit/224c1cf2a421441433097618cc1dd3db224d5915)) +- **user:** Change permission for sample data ([#3462](https://github.com/juspay/hyperswitch/pull/3462)) ([`610c1c5`](https://github.com/juspay/hyperswitch/commit/610c1c575253ddf7a1a31ef941efaae2dd676b48)) + +### Refactors + +- **core:** Restrict requires_customer_action in confirm ([#3235](https://github.com/juspay/hyperswitch/pull/3235)) ([`d2accde`](https://github.com/juspay/hyperswitch/commit/d2accdef410319733d6174057bdca468bde1ae83)) + +### Miscellaneous Tasks + +- **config:** [ADYEN] Add configs for PIX in WASM ([#3498](https://github.com/juspay/hyperswitch/pull/3498)) ([`9821935`](https://github.com/juspay/hyperswitch/commit/9821935933e178765b3b0d0bcbfdf4ab041c3bc2)) + +**Full Changelog:** [`2024.01.30.1...2024.01.31.0`](https://github.com/juspay/hyperswitch/compare/2024.01.30.1...2024.01.31.0) + +- - - + ## 2024.01.30.1 ### Features From 20568dc976687b8b2bfba12ab2db8926cf1c14ed Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:54:25 +0530 Subject: [PATCH 423/443] fix(connector): [Trustpay] add merchant_id in gpay session response for trustpay (#3471) --- crates/router/src/connector/trustpay/transformers.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 4d8e47ab0dc3..c266753423e9 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -1070,6 +1070,7 @@ pub struct GooglePayTransactionInfo { #[serde(rename_all = "camelCase")] pub struct GooglePayMerchantInfo { pub merchant_name: String, + pub merchant_id: String, } #[derive(Clone, Default, Debug, Deserialize)] @@ -1290,7 +1291,7 @@ impl From for api_models::payments::GpayTransactionInf impl From for api_models::payments::GpayMerchantInfo { fn from(value: GooglePayMerchantInfo) -> Self { Self { - merchant_id: None, + merchant_id: Some(value.merchant_id), merchant_name: value.merchant_name, } } From dfb14a34c96ba05e7ff1993fdcd97ee294c02d65 Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:02:00 +0530 Subject: [PATCH 424/443] ci: add test cases for restricting customer action in confirm flow (#3499) --- .../.meta.json | 3 + .../Payments - Confirm/.event.meta.json | 3 + .../Payments - Confirm/event.test.js | 33 +++++++ .../Payments - Confirm/request.json | 79 ++++++++++++++++ .../Payments - Confirm/response.json | 1 + .../.event.meta.json | 3 + .../event.test.js | 80 ++++++++++++++++ .../request.json | 91 +++++++++++++++++++ .../response.json | 1 + 9 files changed, 294 insertions(+) create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/.event.meta.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/event.test.js create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json create mode 100644 postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json new file mode 100644 index 000000000000..343d847414ad --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create with confirm true", "Payments - Confirm"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js new file mode 100644 index 000000000000..018bdf6fc328 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/event.test.js @@ -0,0 +1,33 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 400", function () { + pm.response.to.be.error; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + + +// Response body should have appropriatae error message +if (jsonData?.message) { + pm.test( + "Content check if appropriate error message is present", + function () { + pm.expect(jsonData.message).to.eql("You cannot confirm this payment because it has status requires_customer_action"); + }, + ); +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json new file mode 100644 index 000000000000..1ce1d61eaa15 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/request.json @@ -0,0 +1,79 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "Joseph Doe", + "card_cvc": "123" + } + }, + "client_secret": "{{client_secret}}", + "browser_info": { + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", + "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "language": "nl-NL", + "color_depth": 24, + "screen_height": 723, + "screen_width": 1536, + "time_zone": 0, + "java_enabled": true, + "java_script_enabled": true, + "ip_address": "125.0.0.1" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id", "confirm"], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Confirm/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/.event.meta.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/event.test.js b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/event.test.js new file mode 100644 index 000000000000..39cbb3ee90e9 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/event.test.js @@ -0,0 +1,80 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "requires_customer_action" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'", + function () { + pm.expect(jsonData.status).to.eql("requires_customer_action"); + }, + ); +} + +// Response body should have "next_action.redirect_to_url" +pm.test( + "[POST]::/payments - Content check if 'next_action.redirect_to_url' exists", + function () { + pm.expect(typeof jsonData.next_action.redirect_to_url !== "undefined").to.be + .true; + }, +); diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json new file mode 100644 index 000000000000..d14ae6582c8a --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/request.json @@ -0,0 +1,91 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 6540, + "currency": "USD", + "confirm": true, + "capture_method": "automatic", + "business_country": "US", + "business_label": "default", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 6540, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "three_ds", + "return_url": "https://duck.com", + "setup_future_usage": "on_session", + "payment_method": "card", + "payment_method_data": { + "card": { + "card_number": "4000000000003063", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "sundari" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + }, + "routing": { + "type": "single", + "data": "stripe" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario28-Confirm a payment with requires_customer_action status/Payments - Create with confirm true/response.json @@ -0,0 +1 @@ +[] From e331d2d5569405b89052c6bb59f7e755523f6f15 Mon Sep 17 00:00:00 2001 From: Rachit Naithani <81706961+racnan@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:49:09 +0530 Subject: [PATCH 425/443] feat(users): Added blacklist for users (#3469) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/analytics.rs | 38 +-------- crates/router/src/consts.rs | 6 +- crates/router/src/core/user.rs | 5 ++ crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/user.rs | 14 ++++ crates/router/src/services/authentication.rs | 84 ++++++++++++++++--- .../src/services/authentication/blacklist.rs | 63 ++++++++++++++ crates/router_env/src/logger/types.rs | 2 + 9 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 crates/router/src/services/authentication/blacklist.rs diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 3f0febcc592c..325ca980243c 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -21,7 +21,7 @@ pub mod routes { routes::AppState, services::{ api, - authentication::{self as auth, AuthToken, AuthenticationData}, + authentication::{self as auth, AuthenticationData}, authorization::permissions::Permission, ApplicationResponse, }, @@ -378,23 +378,13 @@ pub mod routes { req: actix_web::HttpRequest, json_payload: web::Json, ) -> impl Responder { - let state_ref = &state; - let req_headers = &req.headers(); - let flow = AnalyticsFlow::GenerateRefundReport; Box::pin(api::server_wrap( flow, state.clone(), &req, json_payload.into_inner(), - |state, auth: AuthenticationData, payload| async move { - let jwt_payload = - auth::parse_jwt_payload::(req_headers, state_ref).await; - - let user_id = jwt_payload - .change_context(AnalyticsError::UnknownError)? - .user_id; - + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { let user = UserInterface::find_user_by_id(&*state.store, &user_id) .await .change_context(AnalyticsError::UnknownError)?; @@ -430,23 +420,13 @@ pub mod routes { req: actix_web::HttpRequest, json_payload: web::Json, ) -> impl Responder { - let state_ref = &state; - let req_headers = &req.headers(); - let flow = AnalyticsFlow::GenerateDisputeReport; Box::pin(api::server_wrap( flow, state.clone(), &req, json_payload.into_inner(), - |state, auth: AuthenticationData, payload| async move { - let jwt_payload = - auth::parse_jwt_payload::(req_headers, state_ref).await; - - let user_id = jwt_payload - .change_context(AnalyticsError::UnknownError)? - .user_id; - + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { let user = UserInterface::find_user_by_id(&*state.store, &user_id) .await .change_context(AnalyticsError::UnknownError)?; @@ -482,23 +462,13 @@ pub mod routes { req: actix_web::HttpRequest, json_payload: web::Json, ) -> impl Responder { - let state_ref = &state; - let req_headers = &req.headers(); - let flow = AnalyticsFlow::GeneratePaymentReport; Box::pin(api::server_wrap( flow, state.clone(), &req, json_payload.into_inner(), - |state, auth: AuthenticationData, payload| async move { - let jwt_payload = - auth::parse_jwt_payload::(req_headers, state_ref).await; - - let user_id = jwt_payload - .change_context(AnalyticsError::UnknownError)? - .user_id; - + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { let user = UserInterface::find_user_by_id(&*state.store, &user_id) .await .change_context(AnalyticsError::UnknownError)?; diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 387da3c06415..12b688e3d3ab 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -66,12 +66,16 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes -#[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days +pub const USER_BLACKLIST_PREFIX: &str = "BU_"; + #[cfg(feature = "email")] pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day +#[cfg(feature = "email")] +pub const EMAIL_TOKEN_BLACKLIST_PREFIX: &str = "BET_"; + #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; #[cfg(feature = "olap")] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index ae66728e140f..24b6eb9d127a 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -264,6 +264,11 @@ pub async fn connect_account( } } +pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> { + auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?; + Ok(ApplicationResponse::StatusOk) +} + pub async fn change_password( state: AppState, request: user_api::ChangePasswordRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 4a726084c2c8..a69220231f56 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -965,6 +965,7 @@ impl User { web::resource("/signin").route(web::post().to(user_signin_without_invite_checks)), ) .service(web::resource("/v2/signin").route(web::post().to(user_signin))) + .service(web::resource("/signout").route(web::post().to(signout))) .service(web::resource("/change_password").route(web::post().to(change_password))) .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 2837c1defa4a..b726c64f0ed8 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -164,6 +164,7 @@ impl From for ApiIdentifier { | Flow::UserSignUp | Flow::UserSignInWithoutInviteChecks | Flow::UserSignIn + | Flow::Signout | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index d4bdcaae87fc..0c2694dc70f8 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -116,6 +116,20 @@ pub async fn user_connect_account( .await } +pub async fn signout(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::Signout; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, user, _| user_core::signout(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn change_password( state: web::Data, http_req: HttpRequest, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 7f1e078ad53d..221106612f35 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -36,6 +36,7 @@ use crate::{ types::domain, utils::OptionExt, }; +pub mod blacklist; #[derive(Clone, Debug)] pub struct AuthenticationData { @@ -333,6 +334,9 @@ where state: &A, ) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } Ok(( UserWithoutMerchantFromToken { @@ -495,6 +499,9 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.0, permissions)?; @@ -521,6 +528,9 @@ where state: &A, ) -> RouterResult<(UserFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.0, permissions)?; @@ -556,6 +566,9 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.required_permission, permissions)?; @@ -585,12 +598,6 @@ where Ok(payload) } -#[derive(serde::Deserialize)] -struct JwtAuthPayloadFetchMerchantAccount { - merchant_id: String, - role_id: String, -} - #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -601,9 +608,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { - let payload = - parse_jwt_payload::(request_headers, state) - .await?; + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.0, permissions)?; @@ -638,6 +646,56 @@ where } } +pub type AuthenticationDataWithUserId = (AuthenticationData, String); + +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithUserId, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id(&payload.merchant_id, &key_store) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + }; + Ok(( + (auth.clone(), payload.user_id.clone()), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.merchant_id.clone(), + user_id: None, + }, + )) + } +} + pub struct DashboardNoPermissionAuth; #[cfg(feature = "olap")] @@ -652,6 +710,9 @@ where state: &A, ) -> RouterResult<(UserFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } Ok(( UserFromToken { @@ -679,7 +740,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<((), AuthenticationType)> { - parse_jwt_payload::(request_headers, state).await?; + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } Ok(((), AuthenticationType::NoAuth)) } diff --git a/crates/router/src/services/authentication/blacklist.rs b/crates/router/src/services/authentication/blacklist.rs new file mode 100644 index 000000000000..6fab28433b48 --- /dev/null +++ b/crates/router/src/services/authentication/blacklist.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +#[cfg(feature = "olap")] +use common_utils::date_time; +use error_stack::{IntoReport, ResultExt}; +use redis_interface::RedisConnectionPool; + +use crate::{ + consts::{JWT_TOKEN_TIME_IN_SECS, USER_BLACKLIST_PREFIX}, + core::errors::{ApiErrorResponse, RouterResult}, + routes::app::AppStateInfo, +}; +#[cfg(feature = "olap")] +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, +}; + +#[cfg(feature = "olap")] +pub async fn insert_user_in_blacklist(state: &AppState, user_id: &str) -> UserResult<()> { + let user_blacklist_key = format!("{}{}", USER_BLACKLIST_PREFIX, user_id); + let expiry = + expiry_to_i64(JWT_TOKEN_TIME_IN_SECS).change_context(UserErrors::InternalServerError)?; + let redis_conn = get_redis_connection(state).change_context(UserErrors::InternalServerError)?; + redis_conn + .set_key_with_expiry( + user_blacklist_key.as_str(), + date_time::now_unix_timestamp(), + expiry, + ) + .await + .change_context(UserErrors::InternalServerError) +} + +pub async fn check_user_in_blacklist( + state: &A, + user_id: &str, + token_expiry: u64, +) -> RouterResult { + let token = format!("{}{}", USER_BLACKLIST_PREFIX, user_id); + let token_issued_at = expiry_to_i64(token_expiry - JWT_TOKEN_TIME_IN_SECS)?; + let redis_conn = get_redis_connection(state)?; + redis_conn + .get_key::>(token.as_str()) + .await + .change_context(ApiErrorResponse::InternalServerError) + .map(|timestamp| timestamp.map_or(false, |timestamp| timestamp > token_issued_at)) +} + +fn get_redis_connection(state: &A) -> RouterResult> { + state + .store() + .get_redis_conn() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection") +} + +fn expiry_to_i64(expiry: u64) -> RouterResult { + expiry + .try_into() + .into_report() + .change_context(ApiErrorResponse::InternalServerError) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 8e32cb633341..a395235ca8a5 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -285,6 +285,8 @@ pub enum Flow { FrmFulfillment, /// Change password flow ChangePassword, + /// Signout flow + Signout, /// Set Dashboard Metadata flow SetDashboardMetadata, /// Get Multiple Dashboard Metadata flow From 7597f3b692124a762c3b212b604938be2d64175a Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Wed, 31 Jan 2024 07:31:15 +0000 Subject: [PATCH 426/443] feat: add deep health check for analytics (#3438) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/analytics/src/clickhouse.rs | 13 ++ crates/analytics/src/health_check.rs | 8 + crates/analytics/src/lib.rs | 1 + crates/analytics/src/sqlx.rs | 12 ++ crates/api_models/Cargo.toml | 1 + crates/api_models/src/health_check.rs | 10 +- crates/router/Cargo.toml | 2 +- .../router/src/compatibility/stripe/errors.rs | 3 +- crates/router/src/core.rs | 1 + .../src/core/errors/api_error_response.rs | 5 + crates/router/src/core/errors/transformers.rs | 5 +- crates/router/src/core/health_check.rs | 111 ++++++++++++++ crates/router/src/core/pm_auth.rs | 3 - crates/router/src/db.rs | 2 +- crates/router/src/db/health_check.rs | 139 ++++-------------- crates/router/src/db/kafka_store.rs | 19 +-- crates/router/src/routes/app.rs | 2 +- crates/router/src/routes/health.rs | 111 +++++++++----- crates/router/src/routes/lock_utils.rs | 2 + crates/router_env/src/logger/types.rs | 2 + crates/storage_impl/src/errors.rs | 4 + 21 files changed, 282 insertions(+), 174 deletions(-) create mode 100644 crates/analytics/src/health_check.rs create mode 100644 crates/router/src/core/health_check.rs diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index 00ae3b6e3103..27d423505090 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -7,6 +7,7 @@ use router_env::logger; use time::PrimitiveDateTime; use super::{ + health_check::HealthCheck, payments::{ distribution::PaymentDistributionRow, filters::FilterRow, metrics::PaymentMetricRow, }, @@ -93,6 +94,18 @@ impl ClickhouseClient { } } +#[async_trait::async_trait] +impl HealthCheck for ClickhouseClient { + async fn deep_health_check( + &self, + ) -> common_utils::errors::CustomResult<(), QueryExecutionError> { + self.execute_query("SELECT 1") + .await + .map(|_| ()) + .change_context(QueryExecutionError::DatabaseError) + } +} + #[async_trait::async_trait] impl AnalyticsDataSource for ClickhouseClient { type Row = serde_json::Value; diff --git a/crates/analytics/src/health_check.rs b/crates/analytics/src/health_check.rs new file mode 100644 index 000000000000..f566aecf10bd --- /dev/null +++ b/crates/analytics/src/health_check.rs @@ -0,0 +1,8 @@ +use common_utils::errors::CustomResult; + +use crate::types::QueryExecutionError; + +#[async_trait::async_trait] +pub trait HealthCheck { + async fn deep_health_check(&self) -> CustomResult<(), QueryExecutionError>; +} diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 501bd58527c3..a4e925519ceb 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -8,6 +8,7 @@ pub mod refunds; pub mod api_event; pub mod connector_events; +pub mod health_check; pub mod outgoing_webhook_event; pub mod sdk_events; mod sqlx; diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 7ab8a2aa4bc5..562a3a1f64d1 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -17,6 +17,7 @@ use storage_impl::config::Database; use time::PrimitiveDateTime; use super::{ + health_check::HealthCheck, query::{Aggregate, ToSql, Window}, types::{ AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, @@ -164,6 +165,17 @@ impl AnalyticsDataSource for SqlxClient { .change_context(QueryExecutionError::RowExtractionFailure) } } +#[async_trait::async_trait] +impl HealthCheck for SqlxClient { + async fn deep_health_check(&self) -> CustomResult<(), QueryExecutionError> { + sqlx::query("SELECT 1") + .fetch_all(&self.pool) + .await + .map(|_| ()) + .into_report() + .change_context(QueryExecutionError::DatabaseError) + } +} impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { fn from_row(row: &'a PgRow) -> sqlx::Result { diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 8cd3ee53f218..1e8e0f47eb94 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -18,6 +18,7 @@ dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"] detailed_errors = [] payouts = [] frm = [] +olap = [] openapi = ["common_enums/openapi"] recon = [] diff --git a/crates/api_models/src/health_check.rs b/crates/api_models/src/health_check.rs index d7bb120d0176..8323f1351346 100644 --- a/crates/api_models/src/health_check.rs +++ b/crates/api_models/src/health_check.rs @@ -1,6 +1,10 @@ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RouterHealthCheckResponse { - pub database: String, - pub redis: String, - pub locker: String, + pub database: bool, + pub redis: bool, + pub locker: bool, + #[cfg(feature = "olap")] + pub analytics: bool, } + +impl common_utils::events::ApiEventMetric for RouterHealthCheckResponse {} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 3d129edfe3f4..f60b0c25e934 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -17,7 +17,7 @@ email = ["external_services/email", "olap"] frm = [] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "aws_s3", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon"] -olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "dep:analytics"] +olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap","api_models/olap","dep:analytics"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 63205ea68ca6..759e968125ff 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -468,7 +468,8 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::MandateUpdateFailed | errors::ApiErrorResponse::MandateSerializationFailed | errors::ApiErrorResponse::MandateDeserializationFailed - | errors::ApiErrorResponse::InternalServerError => Self::InternalServerError, // not a stripe code + | errors::ApiErrorResponse::InternalServerError + | errors::ApiErrorResponse::HealthCheckError { .. } => Self::InternalServerError, // not a stripe code errors::ApiErrorResponse::ExternalConnectorError { code, message, diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 5ae4b0be33da..9bdc493e0786 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -17,6 +17,7 @@ pub mod files; #[cfg(feature = "frm")] pub mod fraud_check; pub mod gsm; +pub mod health_check; pub mod locker_migration; pub mod mandate; pub mod metrics; diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 54ec4ec1e295..023e1f4b7fb3 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -238,6 +238,11 @@ pub enum ApiErrorResponse { WebhookInvalidMerchantSecret, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_19", message = "{message}")] CurrencyNotSupported { message: String }, + #[error(error_type = ErrorType::ServerNotAvailable, code= "HE_00", message = "{component} health check is failiing with error: {message}")] + HealthCheckError { + component: &'static str, + message: String, + }, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_24", message = "Merchant connector account is configured with invalid {config}")] InvalidConnectorConfiguration { config: String }, #[error(error_type = ErrorType::ValidationError, code = "HE_01", message = "Failed to convert currency to minor unit")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index ff764cafed62..0119335b7c45 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -123,7 +123,10 @@ impl ErrorSwitch for ApiErrorRespon }, Self::MandateUpdateFailed | Self::MandateSerializationFailed | Self::MandateDeserializationFailed | Self::InternalServerError => { AER::InternalServerError(ApiError::new("HE", 0, "Something went wrong", None)) - } + }, + Self::HealthCheckError { message,component } => { + AER::InternalServerError(ApiError::new("HE",0,format!("{} health check failed with error: {}",component,message),None)) + }, Self::PayoutFailed { data } => { AER::BadRequest(ApiError::new("CE", 4, "Payout failed while processing with connector.", Some(Extra { data: data.clone(), ..Default::default()}))) }, diff --git a/crates/router/src/core/health_check.rs b/crates/router/src/core/health_check.rs new file mode 100644 index 000000000000..6fc038b82e91 --- /dev/null +++ b/crates/router/src/core/health_check.rs @@ -0,0 +1,111 @@ +#[cfg(feature = "olap")] +use analytics::health_check::HealthCheck; +use error_stack::ResultExt; +use router_env::logger; + +use crate::{ + consts::LOCKER_HEALTH_CALL_PATH, + core::errors::{self, CustomResult}, + routes::app, + services::api as services, +}; + +#[async_trait::async_trait] +pub trait HealthCheckInterface { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>; + async fn health_check_redis(&self) -> CustomResult<(), errors::HealthCheckRedisError>; + async fn health_check_locker(&self) -> CustomResult<(), errors::HealthCheckLockerError>; + #[cfg(feature = "olap")] + async fn health_check_analytics(&self) -> CustomResult<(), errors::HealthCheckDBError>; +} + +#[async_trait::async_trait] +impl HealthCheckInterface for app::AppState { + async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { + let db = &*self.store; + db.health_check_db().await?; + Ok(()) + } + + async fn health_check_redis(&self) -> CustomResult<(), errors::HealthCheckRedisError> { + let db = &*self.store; + let redis_conn = db + .get_redis_conn() + .change_context(errors::HealthCheckRedisError::RedisConnectionError)?; + + redis_conn + .serialize_and_set_key_with_expiry("test_key", "test_value", 30) + .await + .change_context(errors::HealthCheckRedisError::SetFailed)?; + + logger::debug!("Redis set_key was successful"); + + redis_conn + .get_key("test_key") + .await + .change_context(errors::HealthCheckRedisError::GetFailed)?; + + logger::debug!("Redis get_key was successful"); + + redis_conn + .delete_key("test_key") + .await + .change_context(errors::HealthCheckRedisError::DeleteFailed)?; + + logger::debug!("Redis delete_key was successful"); + + Ok(()) + } + + async fn health_check_locker(&self) -> CustomResult<(), errors::HealthCheckLockerError> { + let locker = &self.conf.locker; + if !locker.mock_locker { + let mut url = locker.host_rs.to_owned(); + url.push_str(LOCKER_HEALTH_CALL_PATH); + let request = services::Request::new(services::Method::Get, &url); + services::call_connector_api(self, request) + .await + .change_context(errors::HealthCheckLockerError::FailedToCallLocker)? + .ok(); + } + + logger::debug!("Locker call was successful"); + + Ok(()) + } + + #[cfg(feature = "olap")] + async fn health_check_analytics(&self) -> CustomResult<(), errors::HealthCheckDBError> { + let analytics = &self.pool; + match analytics { + analytics::AnalyticsProvider::Sqlx(client) => client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::SqlxAnalyticsError), + analytics::AnalyticsProvider::Clickhouse(client) => client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError), + analytics::AnalyticsProvider::CombinedCkh(sqlx_client, ckh_client) => { + sqlx_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::SqlxAnalyticsError)?; + ckh_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError) + } + analytics::AnalyticsProvider::CombinedSqlx(sqlx_client, ckh_client) => { + sqlx_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::SqlxAnalyticsError)?; + ckh_client + .deep_health_check() + .await + .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError) + } + } + } +} diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index d805925f3183..9f70cc6baeec 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -375,9 +375,6 @@ async fn store_bank_details_in_payment_methods( .await .change_context(ApiErrorResponse::InternalServerError)?; - #[cfg(not(feature = "kms"))] - let pm_auth_key = pm_auth_key; - let mut update_entries: Vec<(storage::PaymentMethod, storage::PaymentMethodUpdate)> = Vec::new(); let mut new_entries: Vec = Vec::new(); diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index b9d346b7a71f..549001772464 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -110,7 +110,7 @@ pub trait StorageInterface: + user_role::UserRoleInterface + authorization::AuthorizationInterface + user::sample_data::BatchSampleDataInterface - + health_check::HealthCheckInterface + + health_check::HealthCheckDbInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/health_check.rs b/crates/router/src/db/health_check.rs index 73bc2a4321d7..6ebc9dfff5ad 100644 --- a/crates/router/src/db/health_check.rs +++ b/crates/router/src/db/health_check.rs @@ -3,145 +3,66 @@ use diesel_models::ConfigNew; use error_stack::ResultExt; use router_env::logger; -use super::{MockDb, StorageInterface, Store}; +use super::{MockDb, Store}; use crate::{ connection, - consts::LOCKER_HEALTH_CALL_PATH, core::errors::{self, CustomResult}, - routes, - services::api as services, types::storage, }; #[async_trait::async_trait] -pub trait HealthCheckInterface { +pub trait HealthCheckDbInterface { async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>; - async fn health_check_redis( - &self, - db: &dyn StorageInterface, - ) -> CustomResult<(), errors::HealthCheckRedisError>; - async fn health_check_locker( - &self, - state: &routes::AppState, - ) -> CustomResult<(), errors::HealthCheckLockerError>; } #[async_trait::async_trait] -impl HealthCheckInterface for Store { +impl HealthCheckDbInterface for Store { async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { let conn = connection::pg_connection_write(self) .await .change_context(errors::HealthCheckDBError::DBError)?; - let _data = conn - .transaction_async(|conn| { - Box::pin(async move { - let query = - diesel::select(diesel::dsl::sql::("1 + 1")); - let _x: i32 = query.get_result_async(&conn).await.map_err(|err| { - logger::error!(read_err=?err,"Error while reading element in the database"); - errors::HealthCheckDBError::DBReadError - })?; + conn.transaction_async(|conn| async move { + let query = diesel::select(diesel::dsl::sql::("1 + 1")); + let _x: i32 = query.get_result_async(&conn).await.map_err(|err| { + logger::error!(read_err=?err,"Error while reading element in the database"); + errors::HealthCheckDBError::DBReadError + })?; - logger::debug!("Database read was successful"); + logger::debug!("Database read was successful"); - let config = ConfigNew { - key: "test_key".to_string(), - config: "test_value".to_string(), - }; + let config = ConfigNew { + key: "test_key".to_string(), + config: "test_value".to_string(), + }; - config.insert(&conn).await.map_err(|err| { - logger::error!(write_err=?err,"Error while writing to database"); - errors::HealthCheckDBError::DBWriteError - })?; + config.insert(&conn).await.map_err(|err| { + logger::error!(write_err=?err,"Error while writing to database"); + errors::HealthCheckDBError::DBWriteError + })?; - logger::debug!("Database write was successful"); + logger::debug!("Database write was successful"); - storage::Config::delete_by_key(&conn, "test_key").await.map_err(|err| { - logger::error!(delete_err=?err,"Error while deleting element in the database"); - errors::HealthCheckDBError::DBDeleteError - })?; - - logger::debug!("Database delete was successful"); - - Ok::<_, errors::HealthCheckDBError>(()) - }) - }) - .await?; - - Ok(()) - } - - async fn health_check_redis( - &self, - db: &dyn StorageInterface, - ) -> CustomResult<(), errors::HealthCheckRedisError> { - let redis_conn = db - .get_redis_conn() - .change_context(errors::HealthCheckRedisError::RedisConnectionError)?; - - redis_conn - .serialize_and_set_key_with_expiry("test_key", "test_value", 30) - .await - .change_context(errors::HealthCheckRedisError::SetFailed)?; - - logger::debug!("Redis set_key was successful"); - - redis_conn - .get_key("test_key") - .await - .change_context(errors::HealthCheckRedisError::GetFailed)?; - - logger::debug!("Redis get_key was successful"); - - redis_conn - .delete_key("test_key") - .await - .change_context(errors::HealthCheckRedisError::DeleteFailed)?; - - logger::debug!("Redis delete_key was successful"); - - Ok(()) - } - - async fn health_check_locker( - &self, - state: &routes::AppState, - ) -> CustomResult<(), errors::HealthCheckLockerError> { - let locker = &state.conf.locker; - if !locker.mock_locker { - let mut url = locker.host_rs.to_owned(); - url.push_str(LOCKER_HEALTH_CALL_PATH); - let request = services::Request::new(services::Method::Get, &url); - services::call_connector_api(state, request) + storage::Config::delete_by_key(&conn, "test_key") .await - .change_context(errors::HealthCheckLockerError::FailedToCallLocker)? - .ok(); - } + .map_err(|err| { + logger::error!(delete_err=?err,"Error while deleting element in the database"); + errors::HealthCheckDBError::DBDeleteError + })?; + + logger::debug!("Database delete was successful"); - logger::debug!("Locker call was successful"); + Ok::<_, errors::HealthCheckDBError>(()) + }) + .await?; Ok(()) } } #[async_trait::async_trait] -impl HealthCheckInterface for MockDb { +impl HealthCheckDbInterface for MockDb { async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { Ok(()) } - - async fn health_check_redis( - &self, - _: &dyn StorageInterface, - ) -> CustomResult<(), errors::HealthCheckRedisError> { - Ok(()) - } - - async fn health_check_locker( - &self, - _: &routes::AppState, - ) -> CustomResult<(), errors::HealthCheckLockerError> { - Ok(()) - } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index e88d59ea9f39..665a920bcada 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -43,7 +43,7 @@ use crate::{ events::EventInterface, file::FileMetadataInterface, gsm::GsmInterface, - health_check::HealthCheckInterface, + health_check::HealthCheckDbInterface, locker_mock_up::LockerMockUpInterface, mandate::MandateInterface, merchant_account::MerchantAccountInterface, @@ -58,7 +58,6 @@ use crate::{ routing_algorithm::RoutingAlgorithmInterface, MasterKeyInterface, StorageInterface, }, - routes, services::{authentication, kafka::KafkaProducer, Store}, types::{ domain, @@ -2185,22 +2184,8 @@ impl AuthorizationInterface for KafkaStore { } #[async_trait::async_trait] -impl HealthCheckInterface for KafkaStore { +impl HealthCheckDbInterface for KafkaStore { async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError> { self.diesel_store.health_check_db().await } - - async fn health_check_redis( - &self, - db: &dyn StorageInterface, - ) -> CustomResult<(), errors::HealthCheckRedisError> { - self.diesel_store.health_check_redis(db).await - } - - async fn health_check_locker( - &self, - state: &routes::AppState, - ) -> CustomResult<(), errors::HealthCheckLockerError> { - self.diesel_store.health_check_locker(state).await - } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a69220231f56..9e8bee73c286 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -311,7 +311,7 @@ impl Health { web::scope("health") .app_data(web::Data::new(state)) .service(web::resource("").route(web::get().to(health))) - .service(web::resource("/deep_check").route(web::post().to(deep_health_check))) + .service(web::resource("/ready").route(web::get().to(deep_health_check))) } } diff --git a/crates/router/src/routes/health.rs b/crates/router/src/routes/health.rs index f07b744f7f52..89132c3319bf 100644 --- a/crates/router/src/routes/health.rs +++ b/crates/router/src/routes/health.rs @@ -1,9 +1,14 @@ -use actix_web::web; +use actix_web::{web, HttpRequest}; use api_models::health_check::RouterHealthCheckResponse; -use router_env::{instrument, logger, tracing}; +use router_env::{instrument, logger, tracing, Flow}; use super::app; -use crate::{routes::metrics, services}; +use crate::{ + core::{api_locking, health_check::HealthCheckInterface}, + errors::{self, RouterResponse}, + routes::metrics, + services::{api, authentication as auth}, +}; /// . // #[logger::instrument(skip_all, name = "name1", level = "warn", fields( key1 = "val1" ))] #[instrument(skip_all)] @@ -14,58 +19,90 @@ pub async fn health() -> impl actix_web::Responder { actix_web::HttpResponse::Ok().body("health is good") } -#[instrument(skip_all)] -pub async fn deep_health_check(state: web::Data) -> impl actix_web::Responder { +#[instrument(skip_all, fields(flow = ?Flow::DeepHealthCheck))] +pub async fn deep_health_check( + state: web::Data, + request: HttpRequest, +) -> impl actix_web::Responder { metrics::HEALTH_METRIC.add(&metrics::CONTEXT, 1, &[]); - let db = &*state.store; - let mut status_code = 200; + + let flow = Flow::DeepHealthCheck; + + Box::pin(api::server_wrap( + flow, + state, + &request, + (), + |state, _, _| deep_health_check_func(state), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +async fn deep_health_check_func(state: app::AppState) -> RouterResponse { logger::info!("Deep health check was called"); logger::debug!("Database health check begin"); - let db_status = match db.health_check_db().await { - Ok(_) => "Health is good".to_string(), - Err(err) => { - status_code = 500; - err.to_string() - } - }; + let db_status = state.health_check_db().await.map(|_| true).map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Database", + message: err.to_string() + }) + })?; + logger::debug!("Database health check end"); logger::debug!("Redis health check begin"); - let redis_status = match db.health_check_redis(db).await { - Ok(_) => "Health is good".to_string(), - Err(err) => { - status_code = 500; - err.to_string() - } - }; + let redis_status = state + .health_check_redis() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Redis", + message: err.to_string() + }) + })?; logger::debug!("Redis health check end"); logger::debug!("Locker health check begin"); - let locker_status = match db.health_check_locker(&state).await { - Ok(_) => "Health is good".to_string(), - Err(err) => { - status_code = 500; - err.to_string() - } - }; + let locker_status = state + .health_check_locker() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Locker", + message: err.to_string() + }) + })?; + + #[cfg(feature = "olap")] + let analytics_status = state + .health_check_analytics() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Analytics", + message: err.to_string() + }) + })?; logger::debug!("Locker health check end"); - let response = serde_json::to_string(&RouterHealthCheckResponse { + let response = RouterHealthCheckResponse { database: db_status, redis: redis_status, locker: locker_status, - }) - .unwrap_or_default(); - - if status_code == 200 { - services::http_response_json(response) - } else { - services::http_server_error_json_response(response) - } + #[cfg(feature = "olap")] + analytics: analytics_status, + }; + + Ok(api::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b726c64f0ed8..0dfc3b1b3391 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -11,6 +11,7 @@ pub enum ApiIdentifier { Configs, Customers, Ephemeral, + Health, Mandates, PaymentMethods, PaymentMethodAuth, @@ -83,6 +84,7 @@ impl From for ApiIdentifier { Flow::EphemeralKeyCreate | Flow::EphemeralKeyDelete => Self::Ephemeral, + Flow::DeepHealthCheck => Self::Health, Flow::MandatesRetrieve | Flow::MandatesRevoke | Flow::MandatesList => Self::Mandates, Flow::PaymentMethodsCreate diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index a395235ca8a5..55e7bafd8e00 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -54,6 +54,8 @@ pub enum Tag { /// API Flow #[derive(Debug, Display, Clone, PartialEq, Eq)] pub enum Flow { + /// Deep health Check + DeepHealthCheck, /// Merchants account create flow. MerchantsAccountCreate, /// Merchants account retrieve flow. diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index ac3a04e85b2b..2adcdcf8d2e7 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -394,6 +394,10 @@ pub enum HealthCheckDBError { UnknownError, #[error("Error in database transaction")] TransactionError, + #[error("Error while executing query in Sqlx Analytics")] + SqlxAnalyticsError, + #[error("Error while executing query in Clickhouse Analytics")] + ClickhouseAnalyticsError, } impl From for HealthCheckDBError { From db3d53ff1d8b42d107fafe7a6efe7ec9f155d5a0 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:58:37 +0530 Subject: [PATCH 427/443] feat(users): Add `merchant_id` in `EmailToken` and change user status in reset password (#3473) --- crates/diesel_models/src/query/user.rs | 38 ++++++++++---- crates/router/src/core/user.rs | 35 ++++++++----- crates/router/src/db/kafka_store.rs | 10 ++++ crates/router/src/db/user.rs | 62 +++++++++++++++++++++++ crates/router/src/services/email/types.rs | 24 ++++++--- 5 files changed, 140 insertions(+), 29 deletions(-) diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index b4d5976ba294..6fb5b79ddc1e 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -3,7 +3,7 @@ use diesel::{ associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, JoinOnDsl, QueryDsl, }; -use error_stack::{report, IntoReport}; +use error_stack::IntoReport; use router_env::{ logger, tracing::{self, instrument}, @@ -49,19 +49,37 @@ impl User { pub async fn update_by_user_id( conn: &PgPooledConn, user_id: &str, - user: UserUpdate, + user_update: UserUpdate, ) -> StorageResult { - generics::generic_update_with_results::<::Table, _, _, _>( + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( conn, users_dsl::user_id.eq(user_id.to_owned()), - UserUpdateInternal::from(user), + UserUpdateInternal::from(user_update), ) - .await? - .first() - .cloned() - .ok_or_else(|| { - report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user") - }) + .await + } + + pub async fn update_by_user_email( + conn: &PgPooledConn, + user_email: &str, + user_update: UserUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + users_dsl::email.eq(user_email.to_owned()), + UserUpdateInternal::from(user_update), + ) + .await } pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 24b6eb9d127a..7050c9f00244 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,4 +1,6 @@ use api_models::user::{self as user_api, InviteMultipleUserResponse}; +#[cfg(feature = "email")] +use diesel_models::user_role::UserRoleUpdate; use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew}; #[cfg(feature = "email")] use error_stack::IntoReport; @@ -362,18 +364,10 @@ pub async fn reset_password( let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; - //TODO: Create Update by email query - let user_id = state - .store - .find_user_by_email(token.get_email()) - .await - .change_context(UserErrors::InternalServerError)? - .user_id; - - state + let user = state .store - .update_user_by_user_id( - user_id.as_str(), + .update_user_by_email( + token.get_email(), storage_user::UserUpdate::AccountUpdate { name: None, password: Some(hash_password), @@ -384,7 +378,20 @@ pub async fn reset_password( .await .change_context(UserErrors::InternalServerError)?; - //TODO: Update User role status for invited user + if let Some(inviter_merchant_id) = token.get_merchant_id() { + let update_status_result = state + .store + .update_user_role_by_user_id_merchant_id( + user.user_id.clone().as_str(), + inviter_merchant_id, + UserRoleUpdate::UpdateStatus { + status: UserStatus::Active, + modified_by: user.user_id, + }, + ) + .await; + logger::info!(?update_status_result); + } Ok(ApplicationResponse::StatusOk) } @@ -467,7 +474,7 @@ pub async fn invite_user( .store .insert_user_role(UserRoleNew { user_id: new_user.get_user_id().to_owned(), - merchant_id: user_from_token.merchant_id, + merchant_id: user_from_token.merchant_id.clone(), role_id: request.role_id, org_id: user_from_token.org_id, status: invitation_status, @@ -493,6 +500,7 @@ pub async fn invite_user( user_name: domain::UserName::new(new_user.get_name())?, settings: state.conf.clone(), subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id, }; let send_email_result = state .email_client @@ -669,6 +677,7 @@ async fn handle_new_user_invitation( user_name: domain::UserName::new(new_user.get_name())?, settings: state.conf.clone(), subject: "You have been invited to join Hyperswitch Community!", + merchant_id: user_from_token.merchant_id.clone(), }; let send_email_result = state .email_client diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 665a920bcada..0a9030bae29e 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1895,6 +1895,16 @@ impl UserInterface for KafkaStore { .await } + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult { + self.diesel_store + .update_user_by_email(user_email, user) + .await + } + async fn delete_user_by_user_id( &self, user_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index ecd71f7e2c9b..c7c005a0b52b 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -33,6 +33,12 @@ pub trait UserInterface { user: storage::UserUpdate, ) -> CustomResult; + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult; + async fn delete_user_by_user_id( &self, user_id: &str, @@ -92,6 +98,18 @@ impl UserInterface for Store { .into_report() } + async fn update_user_by_email( + &self, + user_email: &str, + user: storage::UserUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::User::update_by_user_email(&conn, user_email, user) + .await + .map_err(Into::into) + .into_report() + } + async fn delete_user_by_user_id( &self, user_id: &str, @@ -229,6 +247,50 @@ impl UserInterface for MockDb { ) } + async fn update_user_by_email( + &self, + user_email: &str, + update_user: storage::UserUpdate, + ) -> CustomResult { + let mut users = self.users.lock().await; + let user_email_pii: common_utils::pii::Email = user_email + .to_string() + .try_into() + .map_err(|_| errors::StorageError::MockDbError)?; + users + .iter_mut() + .find(|user| user.email == user_email_pii) + .map(|user| { + *user = match &update_user { + storage::UserUpdate::VerifyUser => storage::User { + is_verified: true, + ..user.to_owned() + }, + storage::UserUpdate::AccountUpdate { + name, + password, + is_verified, + preferred_merchant_id, + } => storage::User { + name: name.clone().map(Secret::new).unwrap_or(user.name.clone()), + password: password.clone().unwrap_or(user.password.clone()), + is_verified: is_verified.unwrap_or(user.is_verified), + preferred_merchant_id: preferred_merchant_id + .clone() + .or(user.preferred_merchant_id.clone()), + ..user.to_owned() + }, + }; + user.to_owned() + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No user available for user_email = {user_email}" + )) + .into(), + ) + } + async fn delete_user_by_user_id( &self, user_id: &str, diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index d5aa9926130e..c68907c28461 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -92,18 +92,21 @@ Email : {user_email} #[derive(serde::Serialize, serde::Deserialize)] pub struct EmailToken { email: String, + merchant_id: Option, exp: u64, } impl EmailToken { pub async fn new_token( email: domain::UserEmail, + merchant_id: Option, settings: &configs::settings::Settings, ) -> CustomResult { let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS); let exp = jwt::generate_exp(expiration_duration)?.as_secs(); let token_payload = Self { email: email.get_secret().expose(), + merchant_id, exp, }; jwt::generate_jwt(&token_payload, settings).await @@ -112,6 +115,10 @@ impl EmailToken { pub fn get_email(&self) -> &str { self.email.as_str() } + + pub fn get_merchant_id(&self) -> Option<&str> { + self.merchant_id.as_deref() + } } pub fn get_link_with_token( @@ -132,7 +139,7 @@ pub struct VerifyEmail { #[async_trait::async_trait] impl EmailData for VerifyEmail { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; @@ -161,7 +168,7 @@ pub struct ResetPassword { #[async_trait::async_trait] impl EmailData for ResetPassword { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; @@ -191,7 +198,7 @@ pub struct MagicLink { #[async_trait::async_trait] impl EmailData for MagicLink { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) + let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings) .await .change_context(EmailError::TokenGenerationFailure)?; @@ -216,14 +223,19 @@ pub struct InviteUser { pub user_name: domain::UserName, pub settings: std::sync::Arc, pub subject: &'static str, + pub merchant_id: String, } #[async_trait::async_trait] impl EmailData for InviteUser { async fn get_email_data(&self) -> CustomResult { - let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings) - .await - .change_context(EmailError::TokenGenerationFailure)?; + let token = EmailToken::new_token( + self.recipient_email.clone(), + Some(self.merchant_id.clone()), + &self.settings, + ) + .await + .change_context(EmailError::TokenGenerationFailure)?; let invite_user_link = get_link_with_token(&self.settings.email.base_url, token, "set_password"); From a4b97828be103d601a5007f8e4274837faa6886f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:17:20 +0000 Subject: [PATCH 428/443] chore(postman): update Postman collection files --- .../stripe.postman_collection.json | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 0638ff734c4a..847a1323a2c6 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -6053,6 +6053,247 @@ { "name": "Happy Cases", "item": [ + { + "name": "Scenario28-Confirm a payment with requires_customer_action status", + "item": [ + { + "name": "Payments - Create with confirm true", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"requires_customer_action\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'requires_customer_action'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", + " },", + " );", + "}", + "", + "// Response body should have \"next_action.redirect_to_url\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", + " function () {", + " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"business_country\":\"US\",\"business_label\":\"default\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"setup_future_usage\":\"on_session\",\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4000000000003063\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"sundari\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payments - Confirm", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[GET]::/payments/:id - Status code is 400\", function () {", + " pm.response.to.be.error;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[GET]::/payments/:id - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[GET]::/payments/:id - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) { }", + "", + "", + "// Response body should have appropriatae error message", + "if (jsonData?.message) {", + " pm.test(", + " \"Content check if appropriate error message is present\",", + " function () {", + " pm.expect(jsonData.message).to.eql(\"You cannot confirm this payment because it has status requires_customer_action\");", + " },", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{publishable_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"payment_method\":\"card\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"Joseph Doe\",\"card_cvc\":\"123\"}},\"client_secret\":\"{{client_secret}}\",\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"125.0.0.1\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments/:id/confirm", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments", + ":id", + "confirm" + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" + }, + "response": [] + } + ] + }, { "name": "Scenario1-Create payment with confirm true", "item": [ From d2d33c55a9f57c3853dcc3dc1a3c07d7075baeec Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:17:40 +0000 Subject: [PATCH 429/443] chore(version): 2024.01.31.1 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00cfdc5a015a..122730e017ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.01.31.1 + +### Features + +- **users:** + - Added blacklist for users ([#3469](https://github.com/juspay/hyperswitch/pull/3469)) ([`e331d2d`](https://github.com/juspay/hyperswitch/commit/e331d2d5569405b89052c6bb59f7e755523f6f15)) + - Add `merchant_id` in `EmailToken` and change user status in reset password ([#3473](https://github.com/juspay/hyperswitch/pull/3473)) ([`db3d53f`](https://github.com/juspay/hyperswitch/commit/db3d53ff1d8b42d107fafe7a6efe7ec9f155d5a0)) +- Add deep health check for analytics ([#3438](https://github.com/juspay/hyperswitch/pull/3438)) ([`7597f3b`](https://github.com/juspay/hyperswitch/commit/7597f3b692124a762c3b212b604938be2d64175a)) + +### Bug Fixes + +- **connector:** [Trustpay] add merchant_id in gpay session response for trustpay ([#3471](https://github.com/juspay/hyperswitch/pull/3471)) ([`20568dc`](https://github.com/juspay/hyperswitch/commit/20568dc976687b8b2bfba12ab2db8926cf1c14ed)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`a4b9782`](https://github.com/juspay/hyperswitch/commit/a4b97828be103d601a5007f8e4274837faa6886f)) + +**Full Changelog:** [`2024.01.31.0...2024.01.31.1`](https://github.com/juspay/hyperswitch/compare/2024.01.31.0...2024.01.31.1) + +- - - + ## 2024.01.31.0 ### Features From 7f2c434bd29d337dadde8b71a9137797f1c03ec0 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:01:54 +0530 Subject: [PATCH 430/443] feat(pm_list): add required fields for google pay (#3196) Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- crates/router/src/configs/defaults.rs | 189 +++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index b6d8fa114b9a..7b88c9c2dc79 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -142,6 +142,19 @@ impl Default for Mandates { connector_list: HashSet::from([ enums::Connector::Stripe, enums::Connector::Adyen, + enums::Connector::Airwallex, + enums::Connector::Authorizedotnet, + enums::Connector::Bankofamerica, + enums::Connector::Bluesnap, + enums::Connector::Checkout, + enums::Connector::Globalpay, + enums::Connector::Multisafepay, + enums::Connector::Noon, + enums::Connector::Nuvei, + enums::Connector::Payu, + enums::Connector::Rapyd, + enums::Connector::Stripe, + enums::Connector::Trustpay, ]), }, ), @@ -5608,7 +5621,7 @@ impl Default for super::settings::RequiredFields { ConnectorFields { fields: HashMap::from([ ( - enums::Connector::Stripe, + enums::Connector::Adyen, RequiredFieldFinal { mandate: HashMap::new(), non_mandate: HashMap::new(), @@ -5702,6 +5715,146 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bluesnap, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Noon, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Nuvei, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Airwallex, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Authorizedotnet, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Checkout, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Globalpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Multisafepay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ( + "billing.address.line2".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line2".to_string(), + display_name: "line2".to_string(), + field_type: enums::FieldType::UserAddressLine2, + value: None, + } + )]), + common: HashMap::new(), + } + ), ( enums::Connector::Cybersource, RequiredFieldFinal { @@ -5788,7 +5941,39 @@ impl Default for super::settings::RequiredFields { ), common: HashMap::new(), } - ) + ), + ( + enums::Connector::Payu, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Rapyd, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ( + enums::Connector::Trustpay, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), ]), }, ), From 58771b8985a53c83185805f770fee26c5836c645 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:42:17 +0530 Subject: [PATCH 431/443] fix(configs): Add configs for Payme 3DS (#3415) --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/config/config.example.toml b/config/config.example.toml index 00c325f058e6..f7e9fa70f6e3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -352,6 +352,7 @@ bluesnap = { payment_method = "card" } bankofamerica = { payment_method = "card" } cybersource = { payment_method = "card" } nmi = { payment_method = "card" } +payme = {payment_method = "card" } [dummy_connector] enabled = true # Whether dummy connector is enabled or not diff --git a/config/development.toml b/config/development.toml index 681db4ac5d18..584bdf751a2b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -434,6 +434,7 @@ bluesnap = {payment_method = "card"} bankofamerica = {payment_method = "card"} cybersource = {payment_method = "card"} nmi = {payment_method = "card"} +payme = {payment_method = "card"} [connector_customer] connector_list = "gocardless,stax,stripe" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index d468f1dd412f..67fca85929d4 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -245,6 +245,7 @@ bluesnap = {payment_method = "card"} bankofamerica = {payment_method = "card"} cybersource = {payment_method = "card"} nmi = {payment_method = "card"} +payme = {payment_method = "card"} [dummy_connector] enabled = true From 757534104ee0411a887c993e45cc1fb883e82992 Mon Sep 17 00:00:00 2001 From: oscar2d2 Date: Wed, 31 Jan 2024 03:37:29 -0800 Subject: [PATCH 432/443] refactor(connector): [NMI] change error message from not supported to not implemented (#2848) Co-authored-by: swangi-kumari --- crates/router/src/connector/nmi/transformers.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index fcf35bfbe370..5b486aae600c 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -494,10 +494,9 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api_models::payments::WalletData::WeChatPayQr(_) | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "nmi", - }) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nmi"), + )) .into_report() } }, @@ -512,10 +511,9 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) - | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "nmi", - }) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("nmi"), + )) .into_report(), } } From 90a24625ce312e4e7681cf4cc470e6365a052f8a Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:09:35 +0530 Subject: [PATCH 433/443] chore(connector_events_fields): added refund_id, dispute_id to connector events (#3424) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sampras Lopes --- .../clickhouse/scripts/connector_events.sql | 12 +++++++-- .../analytics/src/connector_events/events.rs | 25 +++++++++++++------ .../src/analytics/connector_events.rs | 11 +++----- .../core/fraud_check/flows/checkout_flow.rs | 2 ++ .../fraud_check/flows/fulfillment_flow.rs | 2 ++ .../core/fraud_check/flows/record_return.rs | 2 ++ .../src/core/fraud_check/flows/sale_flow.rs | 2 ++ .../fraud_check/flows/transaction_flow.rs | 2 ++ crates/router/src/core/mandate/utils.rs | 2 ++ crates/router/src/core/payments/helpers.rs | 2 ++ .../router/src/core/payments/transformers.rs | 2 ++ crates/router/src/core/utils.rs | 14 +++++++++++ crates/router/src/core/webhooks/utils.rs | 2 ++ .../router/src/events/connector_api_logs.rs | 6 +++++ crates/router/src/services/api.rs | 2 ++ crates/router/src/types.rs | 7 ++++++ .../router/src/types/api/verify_connector.rs | 2 ++ crates/router/tests/connectors/aci.rs | 4 +++ crates/router/tests/connectors/utils.rs | 2 ++ 19 files changed, 85 insertions(+), 18 deletions(-) diff --git a/crates/analytics/docs/clickhouse/scripts/connector_events.sql b/crates/analytics/docs/clickhouse/scripts/connector_events.sql index 5821cd035567..4a53f9edb0bf 100644 --- a/crates/analytics/docs/clickhouse/scripts/connector_events.sql +++ b/crates/analytics/docs/clickhouse/scripts/connector_events.sql @@ -10,7 +10,9 @@ CREATE TABLE connector_events_queue ( `status_code` UInt32, `created_at` DateTime64(3), `latency` UInt128, - `method` LowCardinality(String) + `method` LowCardinality(String), + `refund_id` Nullable(String), + `dispute_id` Nullable(String) ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-connector-api-events', kafka_group_name = 'hyper-c1', @@ -32,6 +34,8 @@ CREATE TABLE connector_events_dist ( `inserted_at` DateTime64(3), `latency` UInt128, `method` LowCardinality(String), + `refund_id` Nullable(String), + `dispute_id` Nullable(String), INDEX flowIndex flowTYPE bloom_filter GRANULARITY 1, INDEX connectorIndex connector_name TYPE bloom_filter GRANULARITY 1, INDEX statusIndex status_code TYPE bloom_filter GRANULARITY 1 @@ -54,7 +58,9 @@ CREATE MATERIALIZED VIEW connector_events_mv TO connector_events_dist ( `status_code` UInt32, `created_at` DateTime64(3), `latency` UInt128, - `method` LowCardinality(String) + `method` LowCardinality(String), + `refund_id` Nullable(String), + `dispute_id` Nullable(String) ) AS SELECT merchant_id, @@ -70,6 +76,8 @@ SELECT now() as inserted_at, latency, method, + refund_id, + dispute_id FROM connector_events_queue where length(_error) = 0; diff --git a/crates/analytics/src/connector_events/events.rs b/crates/analytics/src/connector_events/events.rs index 096520777eeb..47044811a8bf 100644 --- a/crates/analytics/src/connector_events/events.rs +++ b/crates/analytics/src/connector_events/events.rs @@ -1,7 +1,4 @@ -use api_models::analytics::{ - connector_events::{ConnectorEventsRequest, QueryType}, - Granularity, -}; +use api_models::analytics::{connector_events::ConnectorEventsRequest, Granularity}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; @@ -32,11 +29,23 @@ where query_builder .add_filter_clause("merchant_id", merchant_id) .switch()?; - match query_param.query_param { - QueryType::Payment { payment_id } => query_builder - .add_filter_clause("payment_id", payment_id) - .switch()?, + + query_builder + .add_filter_clause("payment_id", query_param.payment_id) + .switch()?; + + if let Some(refund_id) = query_param.refund_id { + query_builder + .add_filter_clause("refund_id", &refund_id) + .switch()?; + } + + if let Some(dispute_id) = query_param.dispute_id { + query_builder + .add_filter_clause("dispute_id", &dispute_id) + .switch()?; } + //TODO!: update the execute_query function to return reports instead of plain errors... query_builder .execute_query::(pool) diff --git a/crates/api_models/src/analytics/connector_events.rs b/crates/api_models/src/analytics/connector_events.rs index b2974b0a3392..7d7e4ae2a8bc 100644 --- a/crates/api_models/src/analytics/connector_events.rs +++ b/crates/api_models/src/analytics/connector_events.rs @@ -1,11 +1,6 @@ -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(tag = "type")] -pub enum QueryType { - Payment { payment_id: String }, -} - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct ConnectorEventsRequest { - #[serde(flatten)] - pub query_param: QueryType, + pub payment_id: String, + pub refund_id: Option, + pub dispute_id: Option, } diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 7f8993af5270..8b3eed45f9e8 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -119,6 +119,8 @@ impl ConstructFlowSpecificData( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index bd0ba3e4f7f4..2de6aaab7c73 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -97,6 +97,8 @@ impl ConstructFlowSpecificData( external_latency: router_data.external_latency, apple_pay_flow: router_data.apple_pay_flow, frm_metadata: router_data.frm_metadata, + refund_id: router_data.refund_id, + dispute_id: router_data.dispute_id, } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5b05e6015023..1b4b2fc9fc35 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -167,6 +167,8 @@ where external_latency: None, apple_pay_flow, frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 016d5ec955d2..ef8dddde9554 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -176,6 +176,8 @@ pub async fn construct_payout_router_data<'a, F>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) @@ -328,6 +330,8 @@ pub async fn construct_refund_router_data<'a, F>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: Some(refund.refund_id.clone()), + dispute_id: None, }; Ok(router_data) @@ -558,6 +562,8 @@ pub async fn construct_accept_dispute_router_data<'a>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + dispute_id: Some(dispute.dispute_id.clone()), + refund_id: None, }; Ok(router_data) } @@ -646,6 +652,8 @@ pub async fn construct_submit_evidence_router_data<'a>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: Some(dispute.dispute_id.clone()), }; Ok(router_data) } @@ -740,6 +748,8 @@ pub async fn construct_upload_file_router_data<'a>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } @@ -831,6 +841,8 @@ pub async fn construct_defend_dispute_router_data<'a>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: Some(dispute.dispute_id.clone()), }; Ok(router_data) } @@ -915,6 +927,8 @@ pub async fn construct_retrieve_file_router_data<'a>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index 08b490480434..75d37f942798 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -114,6 +114,8 @@ pub async fn construct_webhook_router_data<'a>( external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, }; Ok(router_data) } diff --git a/crates/router/src/events/connector_api_logs.rs b/crates/router/src/events/connector_api_logs.rs index 45c05a3077fd..4d3aadc3c456 100644 --- a/crates/router/src/events/connector_api_logs.rs +++ b/crates/router/src/events/connector_api_logs.rs @@ -18,6 +18,8 @@ pub struct ConnectorEvent { created_at: i128, request_id: String, latency: u128, + refund_id: Option, + dispute_id: Option, status_code: u16, } @@ -34,6 +36,8 @@ impl ConnectorEvent { merchant_id: String, request_id: Option<&RequestId>, latency: u128, + refund_id: Option, + dispute_id: Option, status_code: u16, ) -> Self { Self { @@ -54,6 +58,8 @@ impl ConnectorEvent { .map(|i| i.as_hyphenated().to_string()) .unwrap_or("NO_REQUEST_ID".to_string()), latency, + refund_id, + dispute_id, status_code, } } diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 4251679d5d39..aec0b9bde9e9 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -400,6 +400,8 @@ where req.merchant_id.clone(), state.request_id.as_ref(), external_latency, + req.refund_id.clone(), + req.dispute_id.clone(), status_code, ); diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 531ca849a352..10e13a6af579 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -319,6 +319,9 @@ pub struct RouterData { pub apple_pay_flow: Option, pub frm_metadata: Option, + + pub dispute_id: Option, + pub refund_id: Option, } #[derive(Debug, Clone, serde::Deserialize)] @@ -1467,6 +1470,8 @@ impl From<(&RouterData, T2)> external_latency: data.external_latency, apple_pay_flow: data.apple_pay_flow.clone(), frm_metadata: data.frm_metadata.clone(), + dispute_id: data.dispute_id.clone(), + refund_id: data.refund_id.clone(), } } } @@ -1523,6 +1528,8 @@ impl external_latency: data.external_latency, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index fbd942305845..c03f492e8b8a 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -103,6 +103,8 @@ impl VerifyConnectorData { external_latency: None, apple_pay_flow: None, frm_metadata: None, + refund_id: None, + dispute_id: None, } } } diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index c820b7acd6e4..d3f8147fb262 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -98,6 +98,8 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { apple_pay_flow: None, external_latency: None, frm_metadata: None, + refund_id: None, + dispute_id: None, } } @@ -157,6 +159,8 @@ fn construct_refund_router_data() -> types::RefundsRouterData { apple_pay_flow: None, external_latency: None, frm_metadata: None, + refund_id: None, + dispute_id: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index ed3cdbe31b52..844777e2dcab 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -524,6 +524,8 @@ pub trait ConnectorActions: Connector { apple_pay_flow: None, external_latency: None, frm_metadata: None, + refund_id: None, + dispute_id: None, } } From 94cd7b689758a71e13a3eaa655335e658d13afc8 Mon Sep 17 00:00:00 2001 From: Pritish Budhiraja <1805317@kiit.ac.in> Date: Wed, 31 Jan 2024 17:30:02 +0530 Subject: [PATCH 434/443] feat(dashboard_metadata): Add email alert for Prod Intent (#3482) Co-authored-by: Mani Chandra Dulam Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/router/src/consts/user.rs | 1 + .../src/core/user/dashboard_metadata.rs | 24 ++- .../services/email/assets/bizemailprod.html | 138 ++++++++++++++++++ crates/router/src/services/email/types.rs | 84 ++++++++++- .../src/utils/user/dashboard_metadata.rs | 9 +- 5 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 crates/router/src/services/email/assets/bizemailprod.html diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index c570aca76038..1cda969f780e 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -1,2 +1,3 @@ pub const MAX_NAME_LENGTH: usize = 70; pub const MAX_COMPANY_NAME_LENGTH: usize = 70; +pub const BUSINESS_EMAIL: &str = "biz@hyperswitch.io"; diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index b537aa3ec732..24ff292870e5 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -3,7 +3,11 @@ use diesel_models::{ enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, }; use error_stack::ResultExt; +#[cfg(feature = "email")] +use router_env::logger; +#[cfg(feature = "email")] +use crate::services::email::types as email_types; use crate::{ core::errors::{UserErrors, UserResponse, UserResult}, routes::AppState, @@ -434,15 +438,31 @@ async fn insert_metadata( if utils::is_update_required(&metadata) { metadata = utils::update_user_scoped_metadata( state, - user.user_id, + user.user_id.clone(), user.merchant_id, user.org_id, metadata_key, - data, + data.clone(), ) .await .change_context(UserErrors::InternalServerError); } + + #[cfg(feature = "email")] + { + if utils::is_prod_email_required(&data) { + let email_contents = email_types::BizEmailProd::new(state, data)?; + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + logger::info!(?send_email_result); + } + } + metadata } types::MetaData::SPTestPayment(data) => { diff --git a/crates/router/src/services/email/assets/bizemailprod.html b/crates/router/src/services/email/assets/bizemailprod.html new file mode 100644 index 000000000000..c705608ec720 --- /dev/null +++ b/crates/router/src/services/email/assets/bizemailprod.html @@ -0,0 +1,138 @@ + +Welcome to HyperSwitch! + +
+ + + + + + + + + + + + + + + + + + + + +
+
+

Hi Team,

+

+ A Production Account Intent has been initiated by {username} - + please find more details below: +

+
    +
  1. Name: {username}
  2. +
  3. Point of Contact Email (POC): {poc_email}
  4. +
  5. Legal Business Name: {legal_business_name}
  6. +
  7. Business Location: {business_location}
  8. +
  9. Business Website: {business_website}
  10. +
+
+
+ Regards,
+ Hyperswitch Dashboard Team +
+
+ diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index c68907c28461..6ad1a0eb99ad 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -1,11 +1,16 @@ +use api_models::user::dashboard_metadata::ProdIntent; use common_utils::errors::CustomResult; use error_stack::ResultExt; use external_services::email::{EmailContents, EmailData, EmailError}; -use masking::{ExposeInterface, PeekInterface}; +use masking::{ExposeInterface, PeekInterface, Secret}; -use crate::{configs, consts}; +use crate::{configs, consts, routes::AppState}; #[cfg(feature = "olap")] -use crate::{core::errors::UserErrors, services::jwt, types::domain}; +use crate::{ + core::errors::{UserErrors, UserResult}, + services::jwt, + types::domain, +}; pub enum EmailBody { Verify { @@ -23,6 +28,13 @@ pub enum EmailBody { link: String, user_name: String, }, + BizEmailProd { + user_name: String, + poc_email: String, + legal_business_name: String, + business_location: String, + business_website: String, + }, ReconActivation { user_name: String, }, @@ -69,6 +81,22 @@ pub mod html { username = user_name, ) } + EmailBody::BizEmailProd { + user_name, + poc_email, + legal_business_name, + business_location, + business_website, + } => { + format!( + include_str!("assets/bizemailprod.html"), + poc_email = poc_email, + legal_business_name = legal_business_name, + business_location = business_location, + business_website = business_website, + username = user_name, + ) + } EmailBody::ProFeatureRequest { feature_name, merchant_id, @@ -275,6 +303,56 @@ impl EmailData for ReconActivation { } } +pub struct BizEmailProd { + pub recipient_email: domain::UserEmail, + pub user_name: Secret, + pub poc_email: Secret, + pub legal_business_name: String, + pub business_location: String, + pub business_website: String, + pub settings: std::sync::Arc, + pub subject: &'static str, +} + +impl BizEmailProd { + pub fn new(state: &AppState, data: ProdIntent) -> UserResult { + Ok(Self { + recipient_email: (domain::UserEmail::new( + consts::user::BUSINESS_EMAIL.to_string().into(), + ))?, + settings: state.conf.clone(), + subject: "New Prod Intent", + user_name: data.poc_name.unwrap_or_default().into(), + poc_email: data.poc_email.unwrap_or_default().into(), + legal_business_name: data.legal_business_name.unwrap_or_default(), + business_location: data + .business_location + .unwrap_or(common_enums::CountryAlpha2::AD) + .to_string(), + business_website: data.business_website.unwrap_or_default(), + }) + } +} + +#[async_trait::async_trait] +impl EmailData for BizEmailProd { + async fn get_email_data(&self) -> CustomResult { + let body = html::get_html_body(EmailBody::BizEmailProd { + user_name: self.user_name.clone().expose(), + poc_email: self.poc_email.clone().expose(), + legal_business_name: self.legal_business_name.clone(), + business_location: self.business_location.clone(), + business_website: self.business_website.clone(), + }); + + Ok(EmailContents { + subject: self.subject.to_string(), + body: external_services::email::IntermediateString::new(body), + recipient: self.recipient_email.clone().into_inner(), + }) + } +} + pub struct ProFeatureRequest { pub recipient_email: domain::UserEmail, pub feature_name: String, diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs index 09fb5ccd24b4..bcf270010ea7 100644 --- a/crates/router/src/utils/user/dashboard_metadata.rs +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -2,7 +2,7 @@ use std::{net::IpAddr, str::FromStr}; use actix_web::http::header::HeaderMap; use api_models::user::dashboard_metadata::{ - GetMetaDataRequest, GetMultipleMetaDataPayload, SetMetaDataRequest, + GetMetaDataRequest, GetMultipleMetaDataPayload, ProdIntent, SetMetaDataRequest, }; use diesel_models::{ enums::DashboardMetadata as DBEnum, @@ -276,3 +276,10 @@ pub fn parse_string_to_enums(query: String) -> UserResult bool { + !(data + .poc_email + .as_ref() + .map_or(true, |mail| mail.contains("juspay"))) +} From 7251f6474fdac3575202971e55638c435ca5c4c8 Mon Sep 17 00:00:00 2001 From: Paris Osuch <59344124+parisosuch-dev@users.noreply.github.com> Date: Wed, 31 Jan 2024 06:33:44 -0800 Subject: [PATCH 435/443] refactor(connector): [Paypal] Change error message from NotSupported to NotImplemented (#2877) Co-authored-by: swangi-kumari --- .../src/connector/paypal/transformers.rs | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index baf8f48279d9..88595585fe1b 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -396,11 +396,9 @@ fn get_payment_source( | BankRedirectData::Trustly { .. } | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } - .into()) + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ))? } } } @@ -544,10 +542,9 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | api_models::payments::WalletData::WeChatPayQr(_) | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ))? } }, api::PaymentMethodData::BankRedirect(ref bank_redirection_data) => { @@ -611,10 +608,9 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | api_models::payments::PaymentMethodData::Crypto(_) | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::CardToken(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -629,10 +625,9 @@ impl TryFrom<&api_models::payments::CardRedirectData> for PaypalPaymentsRequest | api_models::payments::CardRedirectData::Benefit {} | api_models::payments::CardRedirectData::MomoAtm {} | api_models::payments::CardRedirectData::CardRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -651,10 +646,9 @@ impl TryFrom<&api_models::payments::PayLaterData> for PaypalPaymentsRequest { | api_models::payments::PayLaterData::WalleyRedirect {} | api_models::payments::PayLaterData::AlmaRedirect {} | api_models::payments::PayLaterData::AtomeRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -669,10 +663,9 @@ impl TryFrom<&api_models::payments::BankDebitData> for PaypalPaymentsRequest { | api_models::payments::BankDebitData::SepaBankDebit { .. } | api_models::payments::BankDebitData::BecsBankDebit { .. } | api_models::payments::BankDebitData::BacsBankDebit { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -696,10 +689,9 @@ impl TryFrom<&api_models::payments::BankTransferData> for PaypalPaymentsRequest | api_models::payments::BankTransferData::MandiriVaBankTransfer { .. } | api_models::payments::BankTransferData::Pix {} | api_models::payments::BankTransferData::Pse {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -724,10 +716,9 @@ impl TryFrom<&api_models::payments::VoucherData> for PaypalPaymentsRequest { | api_models::payments::VoucherData::FamilyMart(_) | api_models::payments::VoucherData::Seicomart(_) | api_models::payments::VoucherData::PayEasy(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } @@ -740,10 +731,9 @@ impl TryFrom<&api_models::payments::GiftCardData> for PaypalPaymentsRequest { match value { api_models::payments::GiftCardData::Givex(_) | api_models::payments::GiftCardData::PaySafeCard {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Paypal", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Paypal"), + ) .into()) } } From b7c0f9aa098c880314a529bc10015256ce2139f7 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Wed, 31 Jan 2024 22:37:47 +0530 Subject: [PATCH 436/443] refactor(connector): [Adyen] change expiresAt time from string to unixtimestamp (#3506) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 6 +-- crates/common_utils/src/custom_serde.rs | 47 +++++++++++++++++++ .../src/connector/adyen/transformers.rs | 15 ++++-- openapi/openapi_spec.json | 2 +- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3b85644553d6..a4f052549bd2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2019,7 +2019,7 @@ pub struct BankTransferNextStepsData { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct VoucherNextStepData { /// Voucher expiry date and time - pub expires_at: Option, + pub expires_at: Option, /// Reference number required for the transaction pub reference: String, /// Url to download the payment instruction @@ -2087,8 +2087,8 @@ pub struct MultibancoTransferInstructions { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct DokuBankTransferInstructions { - #[schema(value_type = String, example = "2023-07-26T17:33:00-07-21")] - pub expires_at: Option, + #[schema(value_type = String, example = "1707091200000")] + pub expires_at: Option, #[schema(value_type = String, example = "122385736258")] pub reference: Secret, #[schema(value_type = String)] diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index edbfa143a667..e4608c4f3714 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -84,6 +84,53 @@ pub mod iso8601 { }) } } + /// Use the well-known ISO 8601 format which is without timezone when serializing and deserializing an + /// [`Option`][PrimitiveDateTime]. + /// + /// [PrimitiveDateTime]: ::time::PrimitiveDateTime + pub mod option_without_timezone { + use serde::{de, Deserialize, Serialize}; + use time::macros::format_description; + + use super::*; + + /// Serialize an [`Option`] using the well-known ISO 8601 format which is without timezone. + pub fn serialize( + date_time: &Option, + serializer: S, + ) -> Result + where + S: Serializer, + { + date_time + .map(|date_time| { + let format = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"); + date_time.assume_utc().format(format) + }) + .transpose() + .map_err(S::Error::custom)? + .serialize(serializer) + } + + /// Deserialize an [`Option`] from its ISO 8601 representation. + pub fn deserialize<'a, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'a>, + { + Option::deserialize(deserializer)? + .map(|time_string| { + let format = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"); + PrimitiveDateTime::parse(time_string, format).map_err(|_| { + de::Error::custom(format!( + "Failed to parse PrimitiveDateTime from {time_string}" + )) + }) + }) + .transpose() + } + } } /// Use the UNIX timestamp when serializing and deserializing an diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 8da5d15c4446..b141634d46ff 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -370,7 +370,12 @@ pub struct AdyenPtsAction { reference: String, download_url: Option, payment_method_type: PaymentType, - expires_at: Option, + #[serde(rename = "expiresAt")] + #[serde( + default, + with = "common_utils::custom_serde::iso8601::option_without_timezone" + )] + expires_at: Option, initial_amount: Option, pass_creation_token: Option, total_amount: Option, @@ -3434,6 +3439,10 @@ pub fn get_present_to_shopper_metadata( response: &PresentToShopperResponse, ) -> errors::CustomResult, errors::ConnectorError> { let reference = response.action.reference.clone(); + let expires_at = response + .action + .expires_at + .map(|time| utils::get_timestamp_in_milliseconds(&time)); match response.action.payment_method_type { PaymentType::Alfamart @@ -3446,7 +3455,7 @@ pub fn get_present_to_shopper_metadata( | PaymentType::Seicomart | PaymentType::PayEasy => { let voucher_data = payments::VoucherNextStepData { - expires_at: response.action.expires_at.clone(), + expires_at, reference, download_url: response.action.download_url.clone(), instructions_url: response.action.instructions_url.clone(), @@ -3470,7 +3479,7 @@ pub fn get_present_to_shopper_metadata( Box::new(payments::DokuBankTransferInstructions { reference: Secret::new(response.action.reference.clone()), instructions_url: response.action.instructions_url.clone(), - expires_at: response.action.expires_at.clone(), + expires_at, }), ); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index d516eeed4cc7..9afd5182529a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7738,7 +7738,7 @@ "properties": { "expires_at": { "type": "string", - "example": "2023-07-26T17:33:00-07-21" + "example": "1707091200000" }, "reference": { "type": "string", From a8c74321dbba5c7be6468fff7680d703d726a781 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 00:19:18 +0000 Subject: [PATCH 437/443] chore(version): 2024.02.01.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122730e017ce..117d4cd90e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.02.01.0 + +### Features + +- **dashboard_metadata:** Add email alert for Prod Intent ([#3482](https://github.com/juspay/hyperswitch/pull/3482)) ([`94cd7b6`](https://github.com/juspay/hyperswitch/commit/94cd7b689758a71e13a3eaa655335e658d13afc8)) +- **pm_list:** Add required fields for google pay ([#3196](https://github.com/juspay/hyperswitch/pull/3196)) ([`7f2c434`](https://github.com/juspay/hyperswitch/commit/7f2c434bd29d337dadde8b71a9137797f1c03ec0)) + +### Bug Fixes + +- **configs:** Add configs for Payme 3DS ([#3415](https://github.com/juspay/hyperswitch/pull/3415)) ([`58771b8`](https://github.com/juspay/hyperswitch/commit/58771b8985a53c83185805f770fee26c5836c645)) + +### Refactors + +- **connector:** + - [NMI] change error message from not supported to not implemented ([#2848](https://github.com/juspay/hyperswitch/pull/2848)) ([`7575341`](https://github.com/juspay/hyperswitch/commit/757534104ee0411a887c993e45cc1fb883e82992)) + - [Paypal] Change error message from NotSupported to NotImplemented ([#2877](https://github.com/juspay/hyperswitch/pull/2877)) ([`7251f64`](https://github.com/juspay/hyperswitch/commit/7251f6474fdac3575202971e55638c435ca5c4c8)) + - [Adyen] change expiresAt time from string to unixtimestamp ([#3506](https://github.com/juspay/hyperswitch/pull/3506)) ([`b7c0f9a`](https://github.com/juspay/hyperswitch/commit/b7c0f9aa098c880314a529bc10015256ce2139f7)) + +### Miscellaneous Tasks + +- **connector_events_fields:** Added refund_id, dispute_id to connector events ([#3424](https://github.com/juspay/hyperswitch/pull/3424)) ([`90a2462`](https://github.com/juspay/hyperswitch/commit/90a24625ce312e4e7681cf4cc470e6365a052f8a)) + +**Full Changelog:** [`2024.01.31.1...2024.02.01.0`](https://github.com/juspay/hyperswitch/compare/2024.01.31.1...2024.02.01.0) + +- - - + ## 2024.01.31.1 ### Features From 7f7f6334e32a272efe6650753080ccc2ba6f2f31 Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:19:43 +0530 Subject: [PATCH 438/443] ci(postman): Stripe collection card expired fix (#3521) Co-authored-by: Likhin Bopanna --- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- .../Payments - Create/request.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json index 550880583066..93bcc23194fe 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario1-Create payment with confirm true/Payments - Create/request.json @@ -41,7 +41,7 @@ "card": { "card_number": "4242424242424242", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "26", "card_holder_name": "joseph Doe", "card_cvc": "123" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json index e37391b78b5c..60e56bb581cc 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario27-Create payment without customer_id and with billing address and shipping address/Payments - Create/request.json @@ -41,7 +41,7 @@ "card": { "card_number": "4242424242424242", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "26", "card_holder_name": "joseph Doe", "card_cvc": "123" } diff --git a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json index 731b249f2aa6..6d1bc5e0a0dd 100644 --- a/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json +++ b/postman/collection-dir/stripe/Flow Testcases/Happy Cases/Scenario8-Create a failure card payment with confirm true/Payments - Create/request.json @@ -41,7 +41,7 @@ "card": { "card_number": "4000000000009995", "card_exp_month": "01", - "card_exp_year": "24", + "card_exp_year": "26", "card_holder_name": "joseph Doe", "card_cvc": "123" } From 20efc3020ac389199eed13154f070685417ef82a Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:23:32 +0530 Subject: [PATCH 439/443] chore: add file storage config in env_specific toml (#3512) Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> --- config/deployments/env_specific.toml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 354c320e8f58..04831376050d 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -70,9 +70,13 @@ api_logs_topic = "topic" # Kafka topic to be used for incoming api connector_logs_topic = "topic" # Kafka topic to be used for connector api events outgoing_webhook_logs_topic = "topic" # Kafka topic to be used for outgoing webhook events -[file_upload_config] -bucket_name = "bucket" -region = "bucket_region" +# File storage configuration +[file_storage] +file_storage_backend = "aws_s3" # File storage backend to be used + +[file_storage.aws_s3] +region = "bucket_region" # The AWS region used by AWS S3 for file storage +bucket_name = "bucket" # The AWS S3 bucket name for file storage # This section provides configs for currency conversion api [forex_api] From 7cf6c8c0b9c4042f2e6b9277b7c75c85546821f7 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:28:55 +0530 Subject: [PATCH 440/443] feat(configs): [Noon] Add applepay mandate configs (#3508) Co-authored-by: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 2 +- config/deployments/production.toml | 2 +- config/deployments/sandbox.toml | 2 +- config/docker_compose.toml | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index f7e9fa70f6e3..007c671e9e43 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -383,6 +383,7 @@ bank_debit.becs = { connector_list = "gocardless" } # Mandate supported payment bank_debit.sepa = { connector_list = "gocardless" } # Mandate supported payment method type and connector for bank_debit bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} # Mandate supported payment method type and connector for bank_redirect bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} +wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon" } # Required fields info used while listing the payment_method_data diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index b84546eff34f..2c5d16e3e3c7 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -114,7 +114,7 @@ bank_debit.sepa.connector_list = "gocardless" card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" pay_later.klarna.connector_list = "adyen" -wallet.apple_pay.connector_list = "stripe,adyen,cybersource" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon" wallet.google_pay.connector_list = "stripe,adyen,cybersource" wallet.paypal.connector_list = "adyen" bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} diff --git a/config/deployments/production.toml b/config/deployments/production.toml index d5479b4f02c1..964281c52bba 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -114,7 +114,7 @@ bank_debit.sepa.connector_list = "gocardless" card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" pay_later.klarna.connector_list = "adyen" -wallet.apple_pay.connector_list = "stripe,adyen,cybersource" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon" wallet.google_pay.connector_list = "stripe,adyen,cybersource" wallet.paypal.connector_list = "adyen" bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 14f49e01caf9..aa2377cf8a08 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -114,7 +114,7 @@ bank_debit.sepa.connector_list = "gocardless" card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon" pay_later.klarna.connector_list = "adyen" -wallet.apple_pay.connector_list = "stripe,adyen,cybersource" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon" wallet.google_pay.connector_list = "stripe,adyen,cybersource" wallet.paypal.connector_list = "adyen" bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 67fca85929d4..fba91dc12547 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -334,7 +334,7 @@ adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,hal [mandates.supported_payment_methods] pay_later.klarna = {connector_list = "adyen"} wallet.google_pay = {connector_list = "stripe,adyen"} -wallet.apple_pay = {connector_list = "stripe,adyen"} +wallet.apple_pay = {connector_list = "stripe,adyen,cybersource,noon"} wallet.paypal = {connector_list = "adyen"} card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon"} From 170e10cb8e0880737585284dd43437f549c019d3 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:44:13 +0530 Subject: [PATCH 441/443] feat: add deep health check for scheduler (#3304) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: dracarys18 --- Cargo.lock | 1 + config/config.example.toml | 6 + config/deployments/scheduler/consumer.toml | 6 + config/deployments/scheduler/producer.toml | 6 + config/development.toml | 5 + config/docker_compose.toml | 5 + crates/api_models/src/health_check.rs | 7 + crates/router/src/bin/scheduler.rs | 125 +++++++++++++++++- .../src/core/errors/api_error_response.rs | 2 +- crates/scheduler/Cargo.toml | 1 + crates/scheduler/src/configs/defaults.rs | 11 ++ crates/scheduler/src/configs/settings.rs | 9 ++ crates/scheduler/src/configs/validations.rs | 12 ++ 13 files changed, 193 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1aabc89a040..f0334ce9cfc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5526,6 +5526,7 @@ dependencies = [ "external_services", "futures 0.3.28", "masking", + "num_cpus", "once_cell", "rand 0.8.5", "redis_interface", diff --git a/config/config.example.toml b/config/config.example.toml index 007c671e9e43..87999f0e9e93 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -298,6 +298,12 @@ lower_fetch_limit = 1800 # Lower limit for fetching entries from redis lock_key = "PRODUCER_LOCKING_KEY" # The following keys defines the producer lock that is created in redis with lock_ttl = 160 # the ttl being the expiry (in seconds) +# Scheduler server configuration +[scheduler.server] +port = 3000 # Port on which the server will listen for incoming requests +host = "127.0.0.1" # Host IP address to bind the server to +workers = 1 # Number of actix workers to handle incoming requests concurrently + batch_size = 200 # Specifies the batch size the producer will push under a single entry in the redis queue # Drainer configuration, which handles draining raw SQL queries from Redis streams to the SQL database diff --git a/config/deployments/scheduler/consumer.toml b/config/deployments/scheduler/consumer.toml index 907e3b8297e3..cdd605526689 100644 --- a/config/deployments/scheduler/consumer.toml +++ b/config/deployments/scheduler/consumer.toml @@ -9,3 +9,9 @@ stream = "scheduler_stream" [scheduler.consumer] consumer_group = "scheduler_group" disabled = false # This flag decides if the consumer should actively consume task + +# Scheduler server configuration +[scheduler.server] +port = 3000 # Port on which the server will listen for incoming requests +host = "127.0.0.1" # Host IP address to bind the server to +workers = 1 # Number of actix workers to handle incoming requests concurrently diff --git a/config/deployments/scheduler/producer.toml b/config/deployments/scheduler/producer.toml index 579466a23cc8..9cbaee96f03c 100644 --- a/config/deployments/scheduler/producer.toml +++ b/config/deployments/scheduler/producer.toml @@ -12,3 +12,9 @@ lock_key = "producer_locking_key" # The following keys defines the producer lock lock_ttl = 160 # the ttl being the expiry (in seconds) lower_fetch_limit = 900 # Lower limit for fetching entries from redis queue (in seconds) upper_fetch_limit = 0 # Upper limit for fetching entries from the redis queue (in seconds)0 + +# Scheduler server configuration +[scheduler.server] +port = 3000 # Port on which the server will listen for incoming requests +host = "127.0.0.1" # Host IP address to bind the server to +workers = 1 # Number of actix workers to handle incoming requests concurrently diff --git a/config/development.toml b/config/development.toml index 584bdf751a2b..20abb7bd6f30 100644 --- a/config/development.toml +++ b/config/development.toml @@ -228,6 +228,11 @@ stream = "SCHEDULER_STREAM" disabled = false consumer_group = "SCHEDULER_GROUP" +[scheduler.server] +port = 3000 +host = "127.0.0.1" +workers = 1 + [email] sender_email = "example@example.com" aws_region = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index fba91dc12547..e6dc01afa741 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -227,6 +227,11 @@ stream = "SCHEDULER_STREAM" disabled = false consumer_group = "SCHEDULER_GROUP" +[scheduler.server] +port = 3000 +host = "127.0.0.1" +workers = 1 + #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } diff --git a/crates/api_models/src/health_check.rs b/crates/api_models/src/health_check.rs index 8323f1351346..eab971b5fe15 100644 --- a/crates/api_models/src/health_check.rs +++ b/crates/api_models/src/health_check.rs @@ -8,3 +8,10 @@ pub struct RouterHealthCheckResponse { } impl common_utils::events::ApiEventMetric for RouterHealthCheckResponse {} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SchedulerHealthCheckResponse { + pub database: bool, + pub redis: bool, +} + +impl common_utils::events::ApiEventMetric for SchedulerHealthCheckResponse {} diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index b800ecb897e5..5f98cd880141 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -1,21 +1,29 @@ #![recursion_limit = "256"] use std::{str::FromStr, sync::Arc}; +use actix_web::{dev::Server, web, Scope}; +use api_models::health_check::SchedulerHealthCheckResponse; use common_utils::ext_traits::{OptionExt, StringExt}; use diesel_models::process_tracker as storage; use error_stack::ResultExt; use router::{ configs::settings::{CmdLineConf, Settings}, - core::errors::{self, CustomResult}, - logger, routes, services, + core::{ + errors::{self, CustomResult}, + health_check::HealthCheckInterface, + }, + logger, routes, + services::{self, api}, types::storage::ProcessTrackerExt, workflows, }; +use router_env::{instrument, tracing}; use scheduler::{ consumer::workflows::ProcessTrackerWorkflow, errors::ProcessTrackerError, workflows::ProcessTrackerWorkflows, SchedulerAppState, }; use serde::{Deserialize, Serialize}; +use storage_impl::errors::ApplicationError; use strum::EnumString; use tokio::sync::{mpsc, oneshot}; @@ -68,6 +76,19 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { [router_env::service_name!()], ); + #[allow(clippy::expect_used)] + let web_server = Box::pin(start_web_server( + state.clone(), + scheduler_flow_str.to_string(), + )) + .await + .expect("Failed to create the server"); + + tokio::spawn(async move { + let _ = web_server.await; + logger::error!("The health check probe stopped working!"); + }); + logger::debug!(startup_config=?state.conf); start_scheduler(&state, scheduler_flow, (tx, rx)).await?; @@ -76,6 +97,106 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { Ok(()) } +pub async fn start_web_server( + state: routes::AppState, + service: String, +) -> errors::ApplicationResult { + let server = state + .conf + .scheduler + .as_ref() + .ok_or(ApplicationError::InvalidConfigurationValueError( + "Scheduler server is invalidly configured".into(), + ))? + .server + .clone(); + + let web_server = actix_web::HttpServer::new(move || { + actix_web::App::new().service(Health::server(state.clone(), service.clone())) + }) + .bind((server.host.as_str(), server.port))? + .workers(server.workers) + .run(); + let _ = web_server.handle(); + + Ok(web_server) +} + +pub struct Health; + +impl Health { + pub fn server(state: routes::AppState, service: String) -> Scope { + web::scope("health") + .app_data(web::Data::new(state)) + .app_data(web::Data::new(service)) + .service(web::resource("").route(web::get().to(health))) + .service(web::resource("/ready").route(web::get().to(deep_health_check))) + } +} + +#[instrument(skip_all)] +pub async fn health() -> impl actix_web::Responder { + logger::info!("Scheduler health was called"); + actix_web::HttpResponse::Ok().body("Scheduler health is good") +} +#[instrument(skip_all)] +pub async fn deep_health_check( + state: web::Data, + service: web::Data, +) -> impl actix_web::Responder { + let report = deep_health_check_func(state, service).await; + match report { + Ok(response) => services::http_response_json( + serde_json::to_string(&response) + .map_err(|err| { + logger::error!(serialization_error=?err); + }) + .unwrap_or_default(), + ), + Err(err) => api::log_and_return_error_response(err), + } +} +#[instrument(skip_all)] +pub async fn deep_health_check_func( + state: web::Data, + service: web::Data, +) -> errors::RouterResult { + logger::info!("{} deep health check was called", service.into_inner()); + + logger::debug!("Database health check begin"); + + let db_status = state.health_check_db().await.map(|_| true).map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Database", + message: err.to_string() + }) + })?; + + logger::debug!("Database health check end"); + + logger::debug!("Redis health check begin"); + + let redis_status = state + .health_check_redis() + .await + .map(|_| true) + .map_err(|err| { + error_stack::report!(errors::ApiErrorResponse::HealthCheckError { + component: "Redis", + message: err.to_string() + }) + })?; + + logger::debug!("Redis health check end"); + + let response = SchedulerHealthCheckResponse { + database: db_status, + redis: redis_status, + }; + + Ok(response) +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, EnumString)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 023e1f4b7fb3..e83483b08169 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -238,7 +238,7 @@ pub enum ApiErrorResponse { WebhookInvalidMerchantSecret, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_19", message = "{message}")] CurrencyNotSupported { message: String }, - #[error(error_type = ErrorType::ServerNotAvailable, code= "HE_00", message = "{component} health check is failiing with error: {message}")] + #[error(error_type = ErrorType::ServerNotAvailable, code= "HE_00", message = "{component} health check is failing with error: {message}")] HealthCheckError { component: &'static str, message: String, diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index fe090552edb3..7d4a7821fc94 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -13,6 +13,7 @@ kv_store = [] async-trait = "0.1.68" error-stack = "0.3.1" futures = "0.3.28" +num_cpus = "1.15.0" once_cell = "1.18.0" rand = "0.8.5" serde = "1.0.193" diff --git a/crates/scheduler/src/configs/defaults.rs b/crates/scheduler/src/configs/defaults.rs index 25eb19e24f2a..d17c20829ea4 100644 --- a/crates/scheduler/src/configs/defaults.rs +++ b/crates/scheduler/src/configs/defaults.rs @@ -6,6 +6,7 @@ impl Default for super::settings::SchedulerSettings { consumer: super::settings::ConsumerSettings::default(), graceful_shutdown_interval: 60000, loop_interval: 5000, + server: super::settings::Server::default(), } } } @@ -30,3 +31,13 @@ impl Default for super::settings::ConsumerSettings { } } } + +impl Default for super::settings::Server { + fn default() -> Self { + Self { + port: 8080, + workers: num_cpus::get_physical(), + host: "localhost".into(), + } + } +} diff --git a/crates/scheduler/src/configs/settings.rs b/crates/scheduler/src/configs/settings.rs index 56a9f4079ac0..723ef81e70c7 100644 --- a/crates/scheduler/src/configs/settings.rs +++ b/crates/scheduler/src/configs/settings.rs @@ -15,6 +15,15 @@ pub struct SchedulerSettings { pub consumer: ConsumerSettings, pub loop_interval: u64, pub graceful_shutdown_interval: u64, + pub server: Server, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(default)] +pub struct Server { + pub port: u16, + pub workers: usize, + pub host: String, } #[derive(Debug, Clone, Deserialize)] diff --git a/crates/scheduler/src/configs/validations.rs b/crates/scheduler/src/configs/validations.rs index e9f6621b2a5a..06052f9ff6c7 100644 --- a/crates/scheduler/src/configs/validations.rs +++ b/crates/scheduler/src/configs/validations.rs @@ -19,6 +19,8 @@ impl super::settings::SchedulerSettings { self.producer.validate()?; + self.server.validate()?; + Ok(()) } } @@ -32,3 +34,13 @@ impl super::settings::ProducerSettings { }) } } + +impl super::settings::Server { + pub fn validate(&self) -> Result<(), ApplicationError> { + common_utils::fp_utils::when(self.host.is_default_or_empty(), || { + Err(ApplicationError::InvalidConfigurationValueError( + "server host must not be empty".into(), + )) + }) + } +} From 54fb61eeebec503f599774fe9e97f6b6ce3f1458 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Thu, 1 Feb 2024 11:44:09 +0000 Subject: [PATCH 442/443] feat: add healthcheck for outgoing request (#3519) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/health_check.rs | 1 + crates/router/src/consts.rs | 3 +++ crates/router/src/core/errors.rs | 6 ++++++ crates/router/src/core/health_check.rs | 23 +++++++++++++++++++++-- crates/router/src/routes/health.rs | 12 ++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/api_models/src/health_check.rs b/crates/api_models/src/health_check.rs index eab971b5fe15..e4611f43bcf8 100644 --- a/crates/api_models/src/health_check.rs +++ b/crates/api_models/src/health_check.rs @@ -5,6 +5,7 @@ pub struct RouterHealthCheckResponse { pub locker: bool, #[cfg(feature = "olap")] pub analytics: bool, + pub outgoing_request: bool, } impl common_utils::events::ApiEventMetric for RouterHealthCheckResponse {} diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 12b688e3d3ab..3c3f01dc5f97 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -91,3 +91,6 @@ pub const MAX_SESSION_EXPIRY: u32 = 7890000; pub const MIN_SESSION_EXPIRY: u32 = 60; pub const LOCKER_HEALTH_CALL_PATH: &str = "/health"; + +// URL for checking the outgoing call +pub const OUTGOING_CALL_URL: &str = "https://api.stripe.com/healthcheck"; diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index cbc4290f63bb..9052893d4a96 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -186,6 +186,12 @@ pub enum ConnectorError { InvalidConnectorConfig { config: &'static str }, } +#[derive(Debug, thiserror::Error)] +pub enum HealthCheckOutGoing { + #[error("Outgoing call failed with error: {message}")] + OutGoingFailed { message: String }, +} + #[derive(Debug, thiserror::Error)] pub enum VaultError { #[error("Failed to save card in card vault")] diff --git a/crates/router/src/core/health_check.rs b/crates/router/src/core/health_check.rs index 6fc038b82e91..bc523b4fba66 100644 --- a/crates/router/src/core/health_check.rs +++ b/crates/router/src/core/health_check.rs @@ -4,7 +4,7 @@ use error_stack::ResultExt; use router_env::logger; use crate::{ - consts::LOCKER_HEALTH_CALL_PATH, + consts, core::errors::{self, CustomResult}, routes::app, services::api as services, @@ -15,6 +15,7 @@ pub trait HealthCheckInterface { async fn health_check_db(&self) -> CustomResult<(), errors::HealthCheckDBError>; async fn health_check_redis(&self) -> CustomResult<(), errors::HealthCheckRedisError>; async fn health_check_locker(&self) -> CustomResult<(), errors::HealthCheckLockerError>; + async fn health_check_outgoing(&self) -> CustomResult<(), errors::HealthCheckOutGoing>; #[cfg(feature = "olap")] async fn health_check_analytics(&self) -> CustomResult<(), errors::HealthCheckDBError>; } @@ -61,7 +62,7 @@ impl HealthCheckInterface for app::AppState { let locker = &self.conf.locker; if !locker.mock_locker { let mut url = locker.host_rs.to_owned(); - url.push_str(LOCKER_HEALTH_CALL_PATH); + url.push_str(consts::LOCKER_HEALTH_CALL_PATH); let request = services::Request::new(services::Method::Get, &url); services::call_connector_api(self, request) .await @@ -108,4 +109,22 @@ impl HealthCheckInterface for app::AppState { } } } + + async fn health_check_outgoing(&self) -> CustomResult<(), errors::HealthCheckOutGoing> { + let request = services::Request::new(services::Method::Get, consts::OUTGOING_CALL_URL); + services::call_connector_api(self, request) + .await + .map_err(|err| errors::HealthCheckOutGoing::OutGoingFailed { + message: err.to_string(), + })? + .map_err(|err| errors::HealthCheckOutGoing::OutGoingFailed { + message: format!( + "Got a non 200 status while making outgoing request. Error {:?}", + err.response + ), + })?; + + logger::debug!("Outgoing request successful"); + Ok(()) + } } diff --git a/crates/router/src/routes/health.rs b/crates/router/src/routes/health.rs index 89132c3319bf..2183ab07fed7 100644 --- a/crates/router/src/routes/health.rs +++ b/crates/router/src/routes/health.rs @@ -94,6 +94,17 @@ async fn deep_health_check_func(state: app::AppState) -> RouterResponse RouterResponse Date: Thu, 1 Feb 2024 18:37:44 +0530 Subject: [PATCH 443/443] refactor(connector): [CYBERSOURCE] Remove default case for Cybersource (#2705) Co-authored-by: swangi-kumari --- .../src/connector/cybersource/transformers.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 9b0bf61c5458..324fe77d0bc5 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -193,9 +193,22 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { utils::get_unimplemented_payment_method_error_message("Cybersource"), ))?, }, - _ => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Cybersource"), - ))?, + payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Cybersource"), + ))? + } }; let processing_information = ProcessingInformation {

e9 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�H=m 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